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 new working tree'])
214
self._check_format(relax_version_check)
216
self.weave_store = WeaveStore(self.controlfilename('weaves'))
217
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
221
return '%s(%r)' % (self.__class__.__name__, self.base)
228
if self._lock_mode or self._lock:
229
from warnings import warn
230
warn("branch %r was not explicitly unlocked" % self)
234
def lock_write(self):
236
if self._lock_mode != 'w':
237
from errors import LockError
238
raise LockError("can't upgrade to a write lock from %r" %
240
self._lock_count += 1
242
from bzrlib.lock import WriteLock
244
self._lock = WriteLock(self.controlfilename('branch-lock'))
245
self._lock_mode = 'w'
251
assert self._lock_mode in ('r', 'w'), \
252
"invalid lock mode %r" % self._lock_mode
253
self._lock_count += 1
255
from bzrlib.lock import ReadLock
257
self._lock = ReadLock(self.controlfilename('branch-lock'))
258
self._lock_mode = 'r'
262
if not self._lock_mode:
263
from errors import LockError
264
raise LockError('branch %r is not locked' % (self))
266
if self._lock_count > 1:
267
self._lock_count -= 1
271
self._lock_mode = self._lock_count = None
273
def abspath(self, name):
274
"""Return absolute filename for something in the branch"""
275
return os.path.join(self.base, name)
277
def relpath(self, path):
278
"""Return path relative to this branch of something inside it.
280
Raises an error if path is not in this branch."""
281
return _relpath(self.base, path)
283
def controlfilename(self, file_or_path):
284
"""Return location relative to branch."""
285
if isinstance(file_or_path, basestring):
286
file_or_path = [file_or_path]
287
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
290
def controlfile(self, file_or_path, mode='r'):
291
"""Open a control file for this branch.
293
There are two classes of file in the control directory: text
294
and binary. binary files are untranslated byte streams. Text
295
control files are stored with Unix newlines and in UTF-8, even
296
if the platform or locale defaults are different.
298
Controlfiles should almost never be opened in write mode but
299
rather should be atomically copied and replaced using atomicfile.
302
fn = self.controlfilename(file_or_path)
304
if mode == 'rb' or mode == 'wb':
305
return file(fn, mode)
306
elif mode == 'r' or mode == 'w':
307
# open in binary mode anyhow so there's no newline translation;
308
# codecs uses line buffering by default; don't want that.
310
return codecs.open(fn, mode + 'b', 'utf-8',
313
raise BzrError("invalid controlfile mode %r" % mode)
315
def _make_control(self):
316
os.mkdir(self.controlfilename([]))
317
self.controlfile('README', 'w').write(
318
"This is a Bazaar-NG control directory.\n"
319
"Do not change any files in this directory.\n")
320
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
321
for d in ('text-store', 'revision-store',
323
os.mkdir(self.controlfilename(d))
324
for f in ('revision-history', 'merged-patches',
325
'pending-merged-patches', 'branch-name',
328
self.controlfile(f, 'w').write('')
329
mutter('created control directory in ' + self.base)
331
# if we want per-tree root ids then this is the place to set
332
# them; they're not needed for now and so ommitted for
334
f = self.controlfile('inventory','w')
335
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
339
def _check_format(self, relax_version_check):
340
"""Check this branch format is supported.
342
The format level is stored, as an integer, in
343
self._branch_format for code that needs to check it later.
345
In the future, we might need different in-memory Branch
346
classes to support downlevel branches. But not yet.
348
fmt = self.controlfile('branch-format', 'r').read()
349
if fmt == BZR_BRANCH_FORMAT_5:
350
self._branch_format = 5
352
elif relax_version_check:
353
if fmt == BZR_BRANCH_FORMAT_4:
354
self._branch_format = 4
357
raise BzrError('sorry, branch format "%s" not supported; '
358
'use a different bzr version, '
359
'or run "bzr upgrade"'
360
% fmt.rstrip('\n\r'))
363
def get_root_id(self):
364
"""Return the id of this branches root"""
365
inv = self.read_working_inventory()
366
return inv.root.file_id
368
def set_root_id(self, file_id):
369
inv = self.read_working_inventory()
370
orig_root_id = inv.root.file_id
371
del inv._byid[inv.root.file_id]
372
inv.root.file_id = file_id
373
inv._byid[inv.root.file_id] = inv.root
376
if entry.parent_id in (None, orig_root_id):
377
entry.parent_id = inv.root.file_id
378
self._write_inventory(inv)
380
def read_working_inventory(self):
381
"""Read the working inventory."""
384
# ElementTree does its own conversion from UTF-8, so open in
386
f = self.controlfile('inventory', 'rb')
387
return bzrlib.xml5.serializer_v5.read_inventory(f)
392
def _write_inventory(self, inv):
393
"""Update the working inventory.
395
That is to say, the inventory describing changes underway, that
396
will be committed to the next revision.
398
from bzrlib.atomicfile import AtomicFile
402
f = AtomicFile(self.controlfilename('inventory'), 'wb')
404
bzrlib.xml5.serializer_v5.write_inventory(inv, f)
411
mutter('wrote working inventory')
414
inventory = property(read_working_inventory, _write_inventory, None,
415
"""Inventory for the working copy.""")
418
def add(self, files, ids=None):
419
"""Make files versioned.
421
Note that the command line normally calls smart_add instead,
422
which can automatically recurse.
424
This puts the files in the Added state, so that they will be
425
recorded by the next commit.
428
List of paths to add, relative to the base of the tree.
431
If set, use these instead of automatically generated ids.
432
Must be the same length as the list of files, but may
433
contain None for ids that are to be autogenerated.
435
TODO: Perhaps have an option to add the ids even if the files do
438
TODO: Perhaps yield the ids and paths as they're added.
440
# TODO: Re-adding a file that is removed in the working copy
441
# should probably put it back with the previous ID.
442
if isinstance(files, basestring):
443
assert(ids is None or isinstance(ids, basestring))
449
ids = [None] * len(files)
451
assert(len(ids) == len(files))
455
inv = self.read_working_inventory()
456
for f,file_id in zip(files, ids):
457
if is_control_file(f):
458
raise BzrError("cannot add control file %s" % quotefn(f))
463
raise BzrError("cannot add top-level %r" % f)
465
fullpath = os.path.normpath(self.abspath(f))
468
kind = file_kind(fullpath)
470
# maybe something better?
471
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
473
if kind != 'file' and kind != 'directory':
474
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
477
file_id = gen_file_id(f)
478
inv.add_path(f, kind=kind, file_id=file_id)
480
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
482
self._write_inventory(inv)
487
def print_file(self, file, revno):
488
"""Print `file` to stdout."""
491
tree = self.revision_tree(self.lookup_revision(revno))
492
# use inventory as it was in that revision
493
file_id = tree.inventory.path2id(file)
495
raise BzrError("%r is not present in revision %s" % (file, revno))
496
tree.print_file(file_id)
501
def remove(self, files, verbose=False):
502
"""Mark nominated files for removal from the inventory.
504
This does not remove their text. This does not run on
506
TODO: Refuse to remove modified files unless --force is given?
508
TODO: Do something useful with directories.
510
TODO: Should this remove the text or not? Tough call; not
511
removing may be useful and the user can just use use rm, and
512
is the opposite of add. Removing it is consistent with most
513
other tools. Maybe an option.
515
## TODO: Normalize names
516
## TODO: Remove nested loops; better scalability
517
if isinstance(files, basestring):
523
tree = self.working_tree()
526
# do this before any modifications
530
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
531
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
533
# having remove it, it must be either ignored or unknown
534
if tree.is_ignored(f):
538
show_status(new_status, inv[fid].kind, quotefn(f))
541
self._write_inventory(inv)
546
# FIXME: this doesn't need to be a branch method
547
def set_inventory(self, new_inventory_list):
548
from bzrlib.inventory import Inventory, InventoryEntry
549
inv = Inventory(self.get_root_id())
550
for path, file_id, parent, kind in new_inventory_list:
551
name = os.path.basename(path)
554
inv.add(InventoryEntry(file_id, name, kind, parent))
555
self._write_inventory(inv)
559
"""Return all unknown files.
561
These are files in the working directory that are not versioned or
562
control files or ignored.
564
>>> b = ScratchBranch(files=['foo', 'foo~'])
565
>>> list(b.unknowns())
568
>>> list(b.unknowns())
571
>>> list(b.unknowns())
574
return self.working_tree().unknowns()
577
def append_revision(self, *revision_ids):
578
from bzrlib.atomicfile import AtomicFile
580
for revision_id in revision_ids:
581
mutter("add {%s} to revision-history" % revision_id)
583
rev_history = self.revision_history()
584
rev_history.extend(revision_ids)
586
f = AtomicFile(self.controlfilename('revision-history'))
588
for rev_id in rev_history:
595
def has_revision(self, revision_id):
596
"""True if this branch has a copy of the revision.
598
This does not necessarily imply the revision is merge
599
or on the mainline."""
600
return revision_id in self.revision_store
603
def get_revision_xml_file(self, revision_id):
604
"""Return XML file object for revision object."""
605
if not revision_id or not isinstance(revision_id, basestring):
606
raise InvalidRevisionId(revision_id)
611
return self.revision_store[revision_id]
613
raise bzrlib.errors.NoSuchRevision(self, revision_id)
618
def get_revision_xml(self, revision_id):
619
return self.get_revision_xml_file(revision_id).read()
622
def get_revision(self, revision_id):
623
"""Return the Revision object for a named revision"""
624
xml_file = self.get_revision_xml_file(revision_id)
627
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
628
except SyntaxError, e:
629
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
633
assert r.revision_id == revision_id
637
def get_revision_delta(self, revno):
638
"""Return the delta for one revision.
640
The delta is relative to its mainline predecessor, or the
641
empty tree for revision 1.
643
assert isinstance(revno, int)
644
rh = self.revision_history()
645
if not (1 <= revno <= len(rh)):
646
raise InvalidRevisionNumber(revno)
648
# revno is 1-based; list is 0-based
650
new_tree = self.revision_tree(rh[revno-1])
652
old_tree = EmptyTree()
654
old_tree = self.revision_tree(rh[revno-2])
656
return compare_trees(old_tree, new_tree)
660
def get_revision_sha1(self, revision_id):
661
"""Hash the stored value of a revision, and return it."""
662
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
665
def get_ancestry(self, revision_id):
666
"""Return a list of revision-ids integrated by a revision.
668
w = self.weave_store.get_weave(ANCESTRY_FILEID)
670
return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
673
def get_inventory_weave(self):
674
return self.weave_store.get_weave(INVENTORY_FILEID)
677
def get_inventory(self, revision_id):
678
"""Get Inventory object by hash."""
679
# FIXME: The text gets passed around a lot coming from the weave.
680
f = StringIO(self.get_inventory_xml(revision_id))
681
return bzrlib.xml5.serializer_v5.read_inventory(f)
684
def get_inventory_xml(self, revision_id):
685
"""Get inventory XML as a file object."""
687
assert isinstance(revision_id, basestring), type(revision_id)
688
iw = self.get_inventory_weave()
689
return iw.get_text(iw.lookup(revision_id))
691
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
694
def get_inventory_sha1(self, revision_id):
695
"""Return the sha1 hash of the inventory entry
697
return self.get_revision(revision_id).inventory_sha1
700
def get_revision_inventory(self, revision_id):
701
"""Return inventory of a past revision."""
702
# bzr 0.0.6 and later imposes the constraint that the inventory_id
703
# must be the same as its revision, so this is trivial.
704
if revision_id == None:
705
return Inventory(self.get_root_id())
707
return self.get_inventory(revision_id)
710
def revision_history(self):
711
"""Return sequence of revision hashes on to this branch.
713
>>> ScratchBranch().revision_history()
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))