1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from cStringIO import StringIO
23
from bzrlib.trace import mutter, note
24
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
26
sha_file, appendpath, file_kind
28
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
29
NoSuchRevision, HistoryMissing, NotBranchError)
30
from bzrlib.textui import show_status
31
from bzrlib.revision import Revision, validate_revision_id
32
from bzrlib.delta import compare_trees
33
from bzrlib.tree import EmptyTree, RevisionTree
34
from bzrlib.inventory import Inventory
35
from bzrlib.weavestore import WeaveStore
36
from bzrlib.store import ImmutableStore
41
INVENTORY_FILEID = '__inventory'
42
ANCESTRY_FILEID = '__ancestry'
45
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
46
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
47
## TODO: Maybe include checks for common corruption of newlines, etc?
50
# TODO: Some operations like log might retrieve the same revisions
51
# repeatedly to calculate deltas. We could perhaps have a weakref
52
# cache in memory to make this faster. In general anything can be
53
# cached in memory between lock and unlock operations.
55
# TODO: please move the revision-string syntax stuff out of the branch
56
# object; it's clutter
59
def find_branch(f, **args):
60
if f and (f.startswith('http://') or f.startswith('https://')):
62
return remotebranch.RemoteBranch(f, **args)
64
return Branch(f, **args)
67
def find_cached_branch(f, cache_root, **args):
68
from remotebranch import RemoteBranch
69
br = find_branch(f, **args)
70
def cacheify(br, store_name):
71
from meta_store import CachedStore
72
cache_path = os.path.join(cache_root, store_name)
74
new_store = CachedStore(getattr(br, store_name), cache_path)
75
setattr(br, store_name, new_store)
77
if isinstance(br, RemoteBranch):
78
cacheify(br, 'inventory_store')
79
cacheify(br, 'text_store')
80
cacheify(br, 'revision_store')
84
def _relpath(base, path):
85
"""Return path relative to base, or raise exception.
87
The path may be either an absolute path or a path relative to the
88
current working directory.
90
Lifted out of Branch.relpath for ease of testing.
92
os.path.commonprefix (python2.4) has a bad bug that it works just
93
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
94
avoids that problem."""
95
rp = os.path.abspath(path)
99
while len(head) >= len(base):
102
head, tail = os.path.split(head)
106
raise NotBranchError("path %r is not within branch %r" % (rp, base))
108
return os.sep.join(s)
111
def find_branch_root(f=None):
112
"""Find the branch root enclosing f, or pwd.
114
f may be a filename or a URL.
116
It is not necessary that f exists.
118
Basically we keep looking up until we find the control directory or
119
run into the root. If there isn't one, raises NotBranchError.
123
elif hasattr(os.path, 'realpath'):
124
f = os.path.realpath(f)
126
f = os.path.abspath(f)
127
if not os.path.exists(f):
128
raise BzrError('%r does not exist' % f)
134
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
136
head, tail = os.path.split(f)
138
# reached the root, whatever that may be
139
raise NotBranchError('%s is not in a branch' % orig_f)
144
# XXX: move into bzrlib.errors; subclass BzrError
145
class DivergedBranches(Exception):
146
def __init__(self, branch1, branch2):
147
self.branch1 = branch1
148
self.branch2 = branch2
149
Exception.__init__(self, "These branches have diverged.")
152
######################################################################
155
class Branch(object):
156
"""Branch holding a history of revisions.
159
Base directory of the branch.
165
If _lock_mode is true, a positive count of the number of times the
169
Lock object from bzrlib.lock.
175
_inventory_weave = None
177
# Map some sort of prefix into a namespace
178
# stuff like "revno:10", "revid:", etc.
179
# This should match a prefix with a function which accepts
180
REVISION_NAMESPACES = {}
182
def __init__(self, base, init=False, find_root=True,
183
relax_version_check=False):
184
"""Create new branch object at a particular location.
186
base -- Base directory for the branch.
188
init -- If True, create new control files in a previously
189
unversioned directory. If False, the branch must already
192
find_root -- If true and init is false, find the root of the
193
existing branch containing base.
195
relax_version_check -- If true, the usual check for the branch
196
version is not applied. This is intended only for
197
upgrade/recovery type use; it's not guaranteed that
198
all operations will work on old format branches.
200
In the test suite, creation of new trees is tested using the
201
`ScratchBranch` class.
204
self.base = os.path.realpath(base)
207
self.base = find_branch_root(base)
209
self.base = os.path.realpath(base)
210
if not isdir(self.controlfilename('.')):
211
raise NotBranchError('not a bzr branch: %s' % quotefn(base),
212
['use "bzr init" to initialize a '
215
self._check_format(relax_version_check)
216
if self._branch_format == 4:
217
self.inventory_store = \
218
ImmutableStore(self.controlfilename('inventory-store'))
220
ImmutableStore(self.controlfilename('text-store'))
221
self.weave_store = WeaveStore(self.controlfilename('weaves'))
222
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
226
return '%s(%r)' % (self.__class__.__name__, self.base)
233
if self._lock_mode or self._lock:
234
from warnings import warn
235
warn("branch %r was not explicitly unlocked" % self)
239
def lock_write(self):
241
if self._lock_mode != 'w':
242
from errors import LockError
243
raise LockError("can't upgrade to a write lock from %r" %
245
self._lock_count += 1
247
from bzrlib.lock import WriteLock
249
self._lock = WriteLock(self.controlfilename('branch-lock'))
250
self._lock_mode = 'w'
256
assert self._lock_mode in ('r', 'w'), \
257
"invalid lock mode %r" % self._lock_mode
258
self._lock_count += 1
260
from bzrlib.lock import ReadLock
262
self._lock = ReadLock(self.controlfilename('branch-lock'))
263
self._lock_mode = 'r'
267
if not self._lock_mode:
268
from errors import LockError
269
raise LockError('branch %r is not locked' % (self))
271
if self._lock_count > 1:
272
self._lock_count -= 1
276
self._lock_mode = self._lock_count = None
278
def abspath(self, name):
279
"""Return absolute filename for something in the branch"""
280
return os.path.join(self.base, name)
282
def relpath(self, path):
283
"""Return path relative to this branch of something inside it.
285
Raises an error if path is not in this branch."""
286
return _relpath(self.base, path)
288
def controlfilename(self, file_or_path):
289
"""Return location relative to branch."""
290
if isinstance(file_or_path, basestring):
291
file_or_path = [file_or_path]
292
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
295
def controlfile(self, file_or_path, mode='r'):
296
"""Open a control file for this branch.
298
There are two classes of file in the control directory: text
299
and binary. binary files are untranslated byte streams. Text
300
control files are stored with Unix newlines and in UTF-8, even
301
if the platform or locale defaults are different.
303
Controlfiles should almost never be opened in write mode but
304
rather should be atomically copied and replaced using atomicfile.
307
fn = self.controlfilename(file_or_path)
309
if mode == 'rb' or mode == 'wb':
310
return file(fn, mode)
311
elif mode == 'r' or mode == 'w':
312
# open in binary mode anyhow so there's no newline translation;
313
# codecs uses line buffering by default; don't want that.
315
return codecs.open(fn, mode + 'b', 'utf-8',
318
raise BzrError("invalid controlfile mode %r" % mode)
320
def _make_control(self):
321
os.mkdir(self.controlfilename([]))
322
self.controlfile('README', 'w').write(
323
"This is a Bazaar-NG control directory.\n"
324
"Do not change any files in this directory.\n")
325
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
326
for d in ('text-store', 'revision-store',
328
os.mkdir(self.controlfilename(d))
329
for f in ('revision-history', 'merged-patches',
330
'pending-merged-patches', 'branch-name',
333
self.controlfile(f, 'w').write('')
334
mutter('created control directory in ' + self.base)
336
# if we want per-tree root ids then this is the place to set
337
# them; they're not needed for now and so ommitted for
339
f = self.controlfile('inventory','w')
340
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
344
def _check_format(self, relax_version_check):
345
"""Check this branch format is supported.
347
The format level is stored, as an integer, in
348
self._branch_format for code that needs to check it later.
350
In the future, we might need different in-memory Branch
351
classes to support downlevel branches. But not yet.
353
fmt = self.controlfile('branch-format', 'r').read()
354
if fmt == BZR_BRANCH_FORMAT_5:
355
self._branch_format = 5
356
elif fmt == BZR_BRANCH_FORMAT_4:
357
self._branch_format = 4
359
if (not relax_version_check
360
and self._branch_format != 5):
361
raise BzrError('sorry, branch format "%s" not supported; '
362
'use a different bzr version, '
363
'or run "bzr upgrade"'
364
% fmt.rstrip('\n\r'))
367
def get_root_id(self):
368
"""Return the id of this branches root"""
369
inv = self.read_working_inventory()
370
return inv.root.file_id
372
def set_root_id(self, file_id):
373
inv = self.read_working_inventory()
374
orig_root_id = inv.root.file_id
375
del inv._byid[inv.root.file_id]
376
inv.root.file_id = file_id
377
inv._byid[inv.root.file_id] = inv.root
380
if entry.parent_id in (None, orig_root_id):
381
entry.parent_id = inv.root.file_id
382
self._write_inventory(inv)
384
def read_working_inventory(self):
385
"""Read the working inventory."""
388
# ElementTree does its own conversion from UTF-8, so open in
390
f = self.controlfile('inventory', 'rb')
391
return bzrlib.xml5.serializer_v5.read_inventory(f)
396
def _write_inventory(self, inv):
397
"""Update the working inventory.
399
That is to say, the inventory describing changes underway, that
400
will be committed to the next revision.
402
from bzrlib.atomicfile import AtomicFile
406
f = AtomicFile(self.controlfilename('inventory'), 'wb')
408
bzrlib.xml5.serializer_v5.write_inventory(inv, f)
415
mutter('wrote working inventory')
418
inventory = property(read_working_inventory, _write_inventory, None,
419
"""Inventory for the working copy.""")
422
def add(self, files, ids=None):
423
"""Make files versioned.
425
Note that the command line normally calls smart_add instead,
426
which can automatically recurse.
428
This puts the files in the Added state, so that they will be
429
recorded by the next commit.
432
List of paths to add, relative to the base of the tree.
435
If set, use these instead of automatically generated ids.
436
Must be the same length as the list of files, but may
437
contain None for ids that are to be autogenerated.
439
TODO: Perhaps have an option to add the ids even if the files do
442
TODO: Perhaps yield the ids and paths as they're added.
444
# TODO: Re-adding a file that is removed in the working copy
445
# should probably put it back with the previous ID.
446
if isinstance(files, basestring):
447
assert(ids is None or isinstance(ids, basestring))
453
ids = [None] * len(files)
455
assert(len(ids) == len(files))
459
inv = self.read_working_inventory()
460
for f,file_id in zip(files, ids):
461
if is_control_file(f):
462
raise BzrError("cannot add control file %s" % quotefn(f))
467
raise BzrError("cannot add top-level %r" % f)
469
fullpath = os.path.normpath(self.abspath(f))
472
kind = file_kind(fullpath)
474
# maybe something better?
475
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
477
if kind != 'file' and kind != 'directory':
478
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
481
file_id = gen_file_id(f)
482
inv.add_path(f, kind=kind, file_id=file_id)
484
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
486
self._write_inventory(inv)
491
def print_file(self, file, revno):
492
"""Print `file` to stdout."""
495
tree = self.revision_tree(self.lookup_revision(revno))
496
# use inventory as it was in that revision
497
file_id = tree.inventory.path2id(file)
499
raise BzrError("%r is not present in revision %s" % (file, revno))
500
tree.print_file(file_id)
505
def remove(self, files, verbose=False):
506
"""Mark nominated files for removal from the inventory.
508
This does not remove their text. This does not run on
510
TODO: Refuse to remove modified files unless --force is given?
512
TODO: Do something useful with directories.
514
TODO: Should this remove the text or not? Tough call; not
515
removing may be useful and the user can just use use rm, and
516
is the opposite of add. Removing it is consistent with most
517
other tools. Maybe an option.
519
## TODO: Normalize names
520
## TODO: Remove nested loops; better scalability
521
if isinstance(files, basestring):
527
tree = self.working_tree()
530
# do this before any modifications
534
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
535
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
537
# having remove it, it must be either ignored or unknown
538
if tree.is_ignored(f):
542
show_status(new_status, inv[fid].kind, quotefn(f))
545
self._write_inventory(inv)
550
# FIXME: this doesn't need to be a branch method
551
def set_inventory(self, new_inventory_list):
552
from bzrlib.inventory import Inventory, InventoryEntry
553
inv = Inventory(self.get_root_id())
554
for path, file_id, parent, kind in new_inventory_list:
555
name = os.path.basename(path)
558
inv.add(InventoryEntry(file_id, name, kind, parent))
559
self._write_inventory(inv)
563
"""Return all unknown files.
565
These are files in the working directory that are not versioned or
566
control files or ignored.
568
>>> b = ScratchBranch(files=['foo', 'foo~'])
569
>>> list(b.unknowns())
572
>>> list(b.unknowns())
575
>>> list(b.unknowns())
578
return self.working_tree().unknowns()
581
def append_revision(self, *revision_ids):
582
from bzrlib.atomicfile import AtomicFile
584
for revision_id in revision_ids:
585
mutter("add {%s} to revision-history" % revision_id)
587
rev_history = self.revision_history()
588
rev_history.extend(revision_ids)
590
f = AtomicFile(self.controlfilename('revision-history'))
592
for rev_id in rev_history:
599
def has_revision(self, revision_id):
600
"""True if this branch has a copy of the revision.
602
This does not necessarily imply the revision is merge
603
or on the mainline."""
604
return revision_id in self.revision_store
607
def get_revision_xml_file(self, revision_id):
608
"""Return XML file object for revision object."""
609
if not revision_id or not isinstance(revision_id, basestring):
610
raise InvalidRevisionId(revision_id)
615
return self.revision_store[revision_id]
617
raise bzrlib.errors.NoSuchRevision(self, revision_id)
622
def get_revision_xml(self, revision_id):
623
return self.get_revision_xml_file(revision_id).read()
626
def get_revision(self, revision_id):
627
"""Return the Revision object for a named revision"""
628
xml_file = self.get_revision_xml_file(revision_id)
631
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
632
except SyntaxError, e:
633
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
637
assert r.revision_id == revision_id
641
def get_revision_delta(self, revno):
642
"""Return the delta for one revision.
644
The delta is relative to its mainline predecessor, or the
645
empty tree for revision 1.
647
assert isinstance(revno, int)
648
rh = self.revision_history()
649
if not (1 <= revno <= len(rh)):
650
raise InvalidRevisionNumber(revno)
652
# revno is 1-based; list is 0-based
654
new_tree = self.revision_tree(rh[revno-1])
656
old_tree = EmptyTree()
658
old_tree = self.revision_tree(rh[revno-2])
660
return compare_trees(old_tree, new_tree)
664
def get_revision_sha1(self, revision_id):
665
"""Hash the stored value of a revision, and return it."""
666
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
669
def get_ancestry(self, revision_id):
670
"""Return a list of revision-ids integrated by a revision.
672
w = self.weave_store.get_weave(ANCESTRY_FILEID)
674
return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
677
def get_inventory_weave(self):
678
return self.weave_store.get_weave(INVENTORY_FILEID)
681
def get_inventory(self, revision_id):
682
"""Get Inventory object by hash."""
683
# FIXME: The text gets passed around a lot coming from the weave.
684
f = StringIO(self.get_inventory_xml(revision_id))
685
return bzrlib.xml5.serializer_v5.read_inventory(f)
688
def get_inventory_xml(self, revision_id):
689
"""Get inventory XML as a file object."""
691
assert isinstance(revision_id, basestring), type(revision_id)
692
iw = self.get_inventory_weave()
693
return iw.get_text(iw.lookup(revision_id))
695
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
698
def get_inventory_sha1(self, revision_id):
699
"""Return the sha1 hash of the inventory entry
701
return self.get_revision(revision_id).inventory_sha1
704
def get_revision_inventory(self, revision_id):
705
"""Return inventory of a past revision."""
706
# bzr 0.0.6 and later imposes the constraint that the inventory_id
707
# must be the same as its revision, so this is trivial.
708
if revision_id == None:
709
return Inventory(self.get_root_id())
711
return self.get_inventory(revision_id)
714
def revision_history(self):
715
"""Return sequence of revision hashes on to this branch."""
718
return [l.rstrip('\r\n') for l in
719
self.controlfile('revision-history', 'r').readlines()]
724
def common_ancestor(self, other, self_revno=None, other_revno=None):
727
>>> sb = ScratchBranch(files=['foo', 'foo~'])
728
>>> sb.common_ancestor(sb) == (None, None)
730
>>> commit.commit(sb, "Committing first revision")
731
>>> sb.common_ancestor(sb)[0]
733
>>> clone = sb.clone()
734
>>> commit.commit(sb, "Committing second revision")
735
>>> sb.common_ancestor(sb)[0]
737
>>> sb.common_ancestor(clone)[0]
739
>>> commit.commit(clone, "Committing divergent second revision")
740
>>> sb.common_ancestor(clone)[0]
742
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
744
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
746
>>> clone2 = sb.clone()
747
>>> sb.common_ancestor(clone2)[0]
749
>>> sb.common_ancestor(clone2, self_revno=1)[0]
751
>>> sb.common_ancestor(clone2, other_revno=1)[0]
754
my_history = self.revision_history()
755
other_history = other.revision_history()
756
if self_revno is None:
757
self_revno = len(my_history)
758
if other_revno is None:
759
other_revno = len(other_history)
760
indices = range(min((self_revno, other_revno)))
763
if my_history[r] == other_history[r]:
764
return r+1, my_history[r]
769
"""Return current revision number for this branch.
771
That is equivalent to the number of revisions committed to
774
return len(self.revision_history())
777
def last_revision(self):
778
"""Return last patch hash, or None if no history.
780
ph = self.revision_history()
787
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
788
"""Return a list of new revisions that would perfectly fit.
790
If self and other have not diverged, return a list of the revisions
791
present in other, but missing from self.
793
>>> from bzrlib.commit import commit
794
>>> bzrlib.trace.silent = True
795
>>> br1 = ScratchBranch()
796
>>> br2 = ScratchBranch()
797
>>> br1.missing_revisions(br2)
799
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
800
>>> br1.missing_revisions(br2)
802
>>> br2.missing_revisions(br1)
804
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
805
>>> br1.missing_revisions(br2)
807
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
808
>>> br1.missing_revisions(br2)
810
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
811
>>> br1.missing_revisions(br2)
812
Traceback (most recent call last):
813
DivergedBranches: These branches have diverged.
815
# FIXME: If the branches have diverged, but the latest
816
# revision in this branch is completely merged into the other,
817
# then we should still be able to pull.
818
self_history = self.revision_history()
819
self_len = len(self_history)
820
other_history = other.revision_history()
821
other_len = len(other_history)
822
common_index = min(self_len, other_len) -1
823
if common_index >= 0 and \
824
self_history[common_index] != other_history[common_index]:
825
raise DivergedBranches(self, other)
827
if stop_revision is None:
828
stop_revision = other_len
830
assert isinstance(stop_revision, int)
831
if stop_revision > other_len:
832
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
834
return other_history[self_len:stop_revision]
837
def update_revisions(self, other, stop_revno=None):
838
"""Pull in new perfect-fit revisions.
840
from bzrlib.fetch import greedy_fetch
843
stop_revision = other.lookup_revision(stop_revno)
846
greedy_fetch(to_branch=self, from_branch=other,
847
revision=stop_revision)
849
pullable_revs = self.missing_revisions(other, stop_revision)
852
greedy_fetch(to_branch=self,
854
revision=pullable_revs[-1])
855
self.append_revision(*pullable_revs)
858
def commit(self, *args, **kw):
859
from bzrlib.commit import Commit
860
Commit().commit(self, *args, **kw)
863
def lookup_revision(self, revision):
864
"""Return the revision identifier for a given revision information."""
865
revno, info = self._get_revision_info(revision)
869
def revision_id_to_revno(self, revision_id):
870
"""Given a revision id, return its revno"""
871
history = self.revision_history()
873
return history.index(revision_id) + 1
875
raise bzrlib.errors.NoSuchRevision(self, revision_id)
878
def get_revision_info(self, revision):
879
"""Return (revno, revision id) for revision identifier.
881
revision can be an integer, in which case it is assumed to be revno (though
882
this will translate negative values into positive ones)
883
revision can also be a string, in which case it is parsed for something like
884
'date:' or 'revid:' etc.
886
revno, rev_id = self._get_revision_info(revision)
888
raise bzrlib.errors.NoSuchRevision(self, revision)
891
def get_rev_id(self, revno, history=None):
892
"""Find the revision id of the specified revno."""
896
history = self.revision_history()
897
elif revno <= 0 or revno > len(history):
898
raise bzrlib.errors.NoSuchRevision(self, revno)
899
return history[revno - 1]
901
def _get_revision_info(self, revision):
902
"""Return (revno, revision id) for revision specifier.
904
revision can be an integer, in which case it is assumed to be revno
905
(though this will translate negative values into positive ones)
906
revision can also be a string, in which case it is parsed for something
907
like 'date:' or 'revid:' etc.
909
A revid is always returned. If it is None, the specifier referred to
910
the null revision. If the revid does not occur in the revision
911
history, revno will be None.
917
try:# Convert to int if possible
918
revision = int(revision)
921
revs = self.revision_history()
922
if isinstance(revision, int):
924
revno = len(revs) + revision + 1
927
rev_id = self.get_rev_id(revno, revs)
928
elif isinstance(revision, basestring):
929
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
930
if revision.startswith(prefix):
931
result = func(self, revs, revision)
933
revno, rev_id = result
936
rev_id = self.get_rev_id(revno, revs)
939
raise BzrError('No namespace registered for string: %r' %
942
raise TypeError('Unhandled revision type %s' % revision)
946
raise bzrlib.errors.NoSuchRevision(self, revision)
949
def _namespace_revno(self, revs, revision):
950
"""Lookup a revision by revision number"""
951
assert revision.startswith('revno:')
953
return (int(revision[6:]),)
956
REVISION_NAMESPACES['revno:'] = _namespace_revno
958
def _namespace_revid(self, revs, revision):
959
assert revision.startswith('revid:')
960
rev_id = revision[len('revid:'):]
962
return revs.index(rev_id) + 1, rev_id
965
REVISION_NAMESPACES['revid:'] = _namespace_revid
967
def _namespace_last(self, revs, revision):
968
assert revision.startswith('last:')
970
offset = int(revision[5:])
975
raise BzrError('You must supply a positive value for --revision last:XXX')
976
return (len(revs) - offset + 1,)
977
REVISION_NAMESPACES['last:'] = _namespace_last
979
def _namespace_tag(self, revs, revision):
980
assert revision.startswith('tag:')
981
raise BzrError('tag: namespace registered, but not implemented.')
982
REVISION_NAMESPACES['tag:'] = _namespace_tag
984
def _namespace_date(self, revs, revision):
985
assert revision.startswith('date:')
987
# Spec for date revisions:
989
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
990
# it can also start with a '+/-/='. '+' says match the first
991
# entry after the given date. '-' is match the first entry before the date
992
# '=' is match the first entry after, but still on the given date.
994
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
995
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
996
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
997
# May 13th, 2005 at 0:00
999
# So the proper way of saying 'give me all entries for today' is:
1000
# -r {date:+today}:{date:-tomorrow}
1001
# The default is '=' when not supplied
1004
if val[:1] in ('+', '-', '='):
1005
match_style = val[:1]
1008
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1009
if val.lower() == 'yesterday':
1010
dt = today - datetime.timedelta(days=1)
1011
elif val.lower() == 'today':
1013
elif val.lower() == 'tomorrow':
1014
dt = today + datetime.timedelta(days=1)
1017
# This should be done outside the function to avoid recompiling it.
1018
_date_re = re.compile(
1019
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1021
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1023
m = _date_re.match(val)
1024
if not m or (not m.group('date') and not m.group('time')):
1025
raise BzrError('Invalid revision date %r' % revision)
1028
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1030
year, month, day = today.year, today.month, today.day
1032
hour = int(m.group('hour'))
1033
minute = int(m.group('minute'))
1034
if m.group('second'):
1035
second = int(m.group('second'))
1039
hour, minute, second = 0,0,0
1041
dt = datetime.datetime(year=year, month=month, day=day,
1042
hour=hour, minute=minute, second=second)
1046
if match_style == '-':
1048
elif match_style == '=':
1049
last = dt + datetime.timedelta(days=1)
1052
for i in range(len(revs)-1, -1, -1):
1053
r = self.get_revision(revs[i])
1054
# TODO: Handle timezone.
1055
dt = datetime.datetime.fromtimestamp(r.timestamp)
1056
if first >= dt and (last is None or dt >= last):
1059
for i in range(len(revs)):
1060
r = self.get_revision(revs[i])
1061
# TODO: Handle timezone.
1062
dt = datetime.datetime.fromtimestamp(r.timestamp)
1063
if first <= dt and (last is None or dt <= last):
1065
REVISION_NAMESPACES['date:'] = _namespace_date
1067
def revision_tree(self, revision_id):
1068
"""Return Tree for a revision on this branch.
1070
`revision_id` may be None for the null revision, in which case
1071
an `EmptyTree` is returned."""
1072
# TODO: refactor this to use an existing revision object
1073
# so we don't need to read it in twice.
1074
if revision_id == None:
1077
inv = self.get_revision_inventory(revision_id)
1078
return RevisionTree(self.weave_store, inv, revision_id)
1081
def working_tree(self):
1082
"""Return a `Tree` for the working copy."""
1083
from workingtree import WorkingTree
1084
return WorkingTree(self.base, self.read_working_inventory())
1087
def basis_tree(self):
1088
"""Return `Tree` object for last revision.
1090
If there are no revisions yet, return an `EmptyTree`.
1092
return self.revision_tree(self.last_revision())
1095
def rename_one(self, from_rel, to_rel):
1098
This can change the directory or the filename or both.
1102
tree = self.working_tree()
1103
inv = tree.inventory
1104
if not tree.has_filename(from_rel):
1105
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1106
if tree.has_filename(to_rel):
1107
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1109
file_id = inv.path2id(from_rel)
1111
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1113
if inv.path2id(to_rel):
1114
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1116
to_dir, to_tail = os.path.split(to_rel)
1117
to_dir_id = inv.path2id(to_dir)
1118
if to_dir_id == None and to_dir != '':
1119
raise BzrError("can't determine destination directory id for %r" % to_dir)
1121
mutter("rename_one:")
1122
mutter(" file_id {%s}" % file_id)
1123
mutter(" from_rel %r" % from_rel)
1124
mutter(" to_rel %r" % to_rel)
1125
mutter(" to_dir %r" % to_dir)
1126
mutter(" to_dir_id {%s}" % to_dir_id)
1128
inv.rename(file_id, to_dir_id, to_tail)
1130
from_abs = self.abspath(from_rel)
1131
to_abs = self.abspath(to_rel)
1133
os.rename(from_abs, to_abs)
1135
raise BzrError("failed to rename %r to %r: %s"
1136
% (from_abs, to_abs, e[1]),
1137
["rename rolled back"])
1139
self._write_inventory(inv)
1144
def move(self, from_paths, to_name):
1147
to_name must exist as a versioned directory.
1149
If to_name exists and is a directory, the files are moved into
1150
it, keeping their old names. If it is a directory,
1152
Note that to_name is only the last component of the new name;
1153
this doesn't change the directory.
1155
This returns a list of (from_path, to_path) pairs for each
1156
entry that is moved.
1161
## TODO: Option to move IDs only
1162
assert not isinstance(from_paths, basestring)
1163
tree = self.working_tree()
1164
inv = tree.inventory
1165
to_abs = self.abspath(to_name)
1166
if not isdir(to_abs):
1167
raise BzrError("destination %r is not a directory" % to_abs)
1168
if not tree.has_filename(to_name):
1169
raise BzrError("destination %r not in working directory" % to_abs)
1170
to_dir_id = inv.path2id(to_name)
1171
if to_dir_id == None and to_name != '':
1172
raise BzrError("destination %r is not a versioned directory" % to_name)
1173
to_dir_ie = inv[to_dir_id]
1174
if to_dir_ie.kind not in ('directory', 'root_directory'):
1175
raise BzrError("destination %r is not a directory" % to_abs)
1177
to_idpath = inv.get_idpath(to_dir_id)
1179
for f in from_paths:
1180
if not tree.has_filename(f):
1181
raise BzrError("%r does not exist in working tree" % f)
1182
f_id = inv.path2id(f)
1184
raise BzrError("%r is not versioned" % f)
1185
name_tail = splitpath(f)[-1]
1186
dest_path = appendpath(to_name, name_tail)
1187
if tree.has_filename(dest_path):
1188
raise BzrError("destination %r already exists" % dest_path)
1189
if f_id in to_idpath:
1190
raise BzrError("can't move %r to a subdirectory of itself" % f)
1192
# OK, so there's a race here, it's possible that someone will
1193
# create a file in this interval and then the rename might be
1194
# left half-done. But we should have caught most problems.
1196
for f in from_paths:
1197
name_tail = splitpath(f)[-1]
1198
dest_path = appendpath(to_name, name_tail)
1199
result.append((f, dest_path))
1200
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1202
os.rename(self.abspath(f), self.abspath(dest_path))
1204
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1205
["rename rolled back"])
1207
self._write_inventory(inv)
1214
def revert(self, filenames, old_tree=None, backups=True):
1215
"""Restore selected files to the versions from a previous tree.
1218
If true (default) backups are made of files before
1221
from bzrlib.errors import NotVersionedError, BzrError
1222
from bzrlib.atomicfile import AtomicFile
1223
from bzrlib.osutils import backup_file
1225
inv = self.read_working_inventory()
1226
if old_tree is None:
1227
old_tree = self.basis_tree()
1228
old_inv = old_tree.inventory
1231
for fn in filenames:
1232
file_id = inv.path2id(fn)
1234
raise NotVersionedError("not a versioned file", fn)
1235
if not old_inv.has_id(file_id):
1236
raise BzrError("file not present in old tree", fn, file_id)
1237
nids.append((fn, file_id))
1239
# TODO: Rename back if it was previously at a different location
1241
# TODO: If given a directory, restore the entire contents from
1242
# the previous version.
1244
# TODO: Make a backup to a temporary file.
1246
# TODO: If the file previously didn't exist, delete it?
1247
for fn, file_id in nids:
1250
f = AtomicFile(fn, 'wb')
1252
f.write(old_tree.get_file(file_id).read())
1258
def pending_merges(self):
1259
"""Return a list of pending merges.
1261
These are revisions that have been merged into the working
1262
directory but not yet committed.
1264
cfn = self.controlfilename('pending-merges')
1265
if not os.path.exists(cfn):
1268
for l in self.controlfile('pending-merges', 'r').readlines():
1269
p.append(l.rstrip('\n'))
1273
def add_pending_merge(self, revision_id):
1274
validate_revision_id(revision_id)
1275
# TODO: Perhaps should check at this point that the
1276
# history of the revision is actually present?
1277
p = self.pending_merges()
1278
if revision_id in p:
1280
p.append(revision_id)
1281
self.set_pending_merges(p)
1284
def set_pending_merges(self, rev_list):
1285
from bzrlib.atomicfile import AtomicFile
1288
f = AtomicFile(self.controlfilename('pending-merges'))
1299
def get_parent(self):
1300
"""Return the parent location of the branch.
1302
This is the default location for push/pull/missing. The usual
1303
pattern is that the user can override it by specifying a
1307
_locs = ['parent', 'pull', 'x-pull']
1310
return self.controlfile(l, 'r').read().strip('\n')
1312
if e.errno != errno.ENOENT:
1317
def set_parent(self, url):
1318
# TODO: Maybe delete old location files?
1319
from bzrlib.atomicfile import AtomicFile
1322
f = AtomicFile(self.controlfilename('parent'))
1331
def check_revno(self, revno):
1333
Check whether a revno corresponds to any revision.
1334
Zero (the NULL revision) is considered valid.
1337
self.check_real_revno(revno)
1339
def check_real_revno(self, revno):
1341
Check whether a revno corresponds to a real revision.
1342
Zero (the NULL revision) is considered invalid
1344
if revno < 1 or revno > self.revno():
1345
raise InvalidRevisionNumber(revno)
1350
class ScratchBranch(Branch):
1351
"""Special test class: a branch that cleans up after itself.
1353
>>> b = ScratchBranch()
1361
def __init__(self, files=[], dirs=[], base=None):
1362
"""Make a test branch.
1364
This creates a temporary directory and runs init-tree in it.
1366
If any files are listed, they are created in the working copy.
1368
from tempfile import mkdtemp
1373
Branch.__init__(self, base, init=init)
1375
os.mkdir(self.abspath(d))
1378
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1383
>>> orig = ScratchBranch(files=["file1", "file2"])
1384
>>> clone = orig.clone()
1385
>>> os.path.samefile(orig.base, clone.base)
1387
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1390
from shutil import copytree
1391
from tempfile import mkdtemp
1394
copytree(self.base, base, symlinks=True)
1395
return ScratchBranch(base=base)
1403
"""Destroy the test branch, removing the scratch directory."""
1404
from shutil import rmtree
1407
mutter("delete ScratchBranch %s" % self.base)
1410
# Work around for shutil.rmtree failing on Windows when
1411
# readonly files are encountered
1412
mutter("hit exception in destroying ScratchBranch: %s" % e)
1413
for root, dirs, files in os.walk(self.base, topdown=False):
1415
os.chmod(os.path.join(root, name), 0700)
1421
######################################################################
1425
def is_control_file(filename):
1426
## FIXME: better check
1427
filename = os.path.normpath(filename)
1428
while filename != '':
1429
head, tail = os.path.split(filename)
1430
## mutter('check %r for control file' % ((head, tail), ))
1431
if tail == bzrlib.BZRDIR:
1433
if filename == head:
1440
def gen_file_id(name):
1441
"""Return new file id.
1443
This should probably generate proper UUIDs, but for the moment we
1444
cope with just randomness because running uuidgen every time is
1447
from binascii import hexlify
1448
from time import time
1450
# get last component
1451
idx = name.rfind('/')
1453
name = name[idx+1 : ]
1454
idx = name.rfind('\\')
1456
name = name[idx+1 : ]
1458
# make it not a hidden file
1459
name = name.lstrip('.')
1461
# remove any wierd characters; we don't escape them but rather
1462
# just pull them out
1463
name = re.sub(r'[^\w.]', '', name)
1465
s = hexlify(rand_bytes(8))
1466
return '-'.join((name, compact_date(time()), s))
1470
"""Return a new tree-root file id."""
1471
return gen_file_id('TREE_ROOT')
1474
def pull_loc(branch):
1475
# TODO: Should perhaps just make attribute be 'base' in
1476
# RemoteBranch and Branch?
1477
if hasattr(branch, "baseurl"):
1478
return branch.baseurl
1483
def copy_branch(branch_from, to_location, revision=None):
1484
"""Copy branch_from into the existing directory to_location.
1487
If not None, only revisions up to this point will be copied.
1488
The head of the new branch will be that revision. Can be a
1492
The name of a local directory that exists but is empty.
1494
# TODO: This could be done *much* more efficiently by just copying
1495
# all the whole weaves and revisions, rather than getting one
1496
# revision at a time.
1497
from bzrlib.merge import merge
1498
from bzrlib.branch import Branch
1500
assert isinstance(branch_from, Branch)
1501
assert isinstance(to_location, basestring)
1503
br_to = Branch(to_location, init=True)
1504
br_to.set_root_id(branch_from.get_root_id())
1505
if revision is None:
1508
revno, rev_id = branch_from.get_revision_info(revision)
1509
br_to.update_revisions(branch_from, stop_revno=revno)
1510
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1511
check_clean=False, ignore_zero=True)
1513
from_location = pull_loc(branch_from)
1514
br_to.set_parent(pull_loc(branch_from))