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
22
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
27
from bzrlib.textui import show_status
28
from bzrlib.revision import Revision
29
from bzrlib.xml import unpack_xml
30
from bzrlib.delta import compare_trees
31
from bzrlib.tree import EmptyTree, RevisionTree
33
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
34
## TODO: Maybe include checks for common corruption of newlines, etc?
37
# TODO: Some operations like log might retrieve the same revisions
38
# repeatedly to calculate deltas. We could perhaps have a weakref
39
# cache in memory to make this faster.
42
def find_branch(f, **args):
43
if f and (f.startswith('http://') or f.startswith('https://')):
45
return remotebranch.RemoteBranch(f, **args)
47
return Branch(f, **args)
50
def find_cached_branch(f, cache_root, **args):
51
from remotebranch import RemoteBranch
52
br = find_branch(f, **args)
53
def cacheify(br, store_name):
54
from meta_store import CachedStore
55
cache_path = os.path.join(cache_root, store_name)
57
new_store = CachedStore(getattr(br, store_name), cache_path)
58
setattr(br, store_name, new_store)
60
if isinstance(br, RemoteBranch):
61
cacheify(br, 'inventory_store')
62
cacheify(br, 'text_store')
63
cacheify(br, 'revision_store')
67
def _relpath(base, path):
68
"""Return path relative to base, or raise exception.
70
The path may be either an absolute path or a path relative to the
71
current working directory.
73
Lifted out of Branch.relpath for ease of testing.
75
os.path.commonprefix (python2.4) has a bad bug that it works just
76
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
77
avoids that problem."""
78
rp = os.path.abspath(path)
82
while len(head) >= len(base):
85
head, tail = os.path.split(head)
89
from errors import NotBranchError
90
raise NotBranchError("path %r is not within branch %r" % (rp, base))
95
def find_branch_root(f=None):
96
"""Find the branch root enclosing f, or pwd.
98
f may be a filename or a URL.
100
It is not necessary that f exists.
102
Basically we keep looking up until we find the control directory or
103
run into the root."""
106
elif hasattr(os.path, 'realpath'):
107
f = os.path.realpath(f)
109
f = os.path.abspath(f)
110
if not os.path.exists(f):
111
raise BzrError('%r does not exist' % f)
117
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
119
head, tail = os.path.split(f)
121
# reached the root, whatever that may be
122
raise BzrError('%r is not in a branch' % orig_f)
125
class DivergedBranches(Exception):
126
def __init__(self, branch1, branch2):
127
self.branch1 = branch1
128
self.branch2 = branch2
129
Exception.__init__(self, "These branches have diverged.")
132
class NoSuchRevision(BzrError):
133
def __init__(self, branch, revision):
135
self.revision = revision
136
msg = "Branch %s has no revision %d" % (branch, revision)
137
BzrError.__init__(self, msg)
140
######################################################################
143
class Branch(object):
144
"""Branch holding a history of revisions.
147
Base directory of the branch.
153
If _lock_mode is true, a positive count of the number of times the
157
Lock object from bzrlib.lock.
164
# Map some sort of prefix into a namespace
165
# stuff like "revno:10", "revid:", etc.
166
# This should match a prefix with a function which accepts
167
REVISION_NAMESPACES = {}
169
def __init__(self, base, init=False, find_root=True):
170
"""Create new branch object at a particular location.
172
base -- Base directory for the branch.
174
init -- If True, create new control files in a previously
175
unversioned directory. If False, the branch must already
178
find_root -- If true and init is false, find the root of the
179
existing branch containing base.
181
In the test suite, creation of new trees is tested using the
182
`ScratchBranch` class.
184
from bzrlib.store import ImmutableStore
186
self.base = os.path.realpath(base)
189
self.base = find_branch_root(base)
191
self.base = os.path.realpath(base)
192
if not isdir(self.controlfilename('.')):
193
from errors import NotBranchError
194
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
195
['use "bzr init" to initialize a new working tree',
196
'current bzr can only operate from top-of-tree'])
199
self.text_store = ImmutableStore(self.controlfilename('text-store'))
200
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
201
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
205
return '%s(%r)' % (self.__class__.__name__, self.base)
212
if self._lock_mode or self._lock:
213
from warnings import warn
214
warn("branch %r was not explicitly unlocked" % self)
219
def lock_write(self):
221
if self._lock_mode != 'w':
222
from errors import LockError
223
raise LockError("can't upgrade to a write lock from %r" %
225
self._lock_count += 1
227
from bzrlib.lock import WriteLock
229
self._lock = WriteLock(self.controlfilename('branch-lock'))
230
self._lock_mode = 'w'
237
assert self._lock_mode in ('r', 'w'), \
238
"invalid lock mode %r" % self._lock_mode
239
self._lock_count += 1
241
from bzrlib.lock import ReadLock
243
self._lock = ReadLock(self.controlfilename('branch-lock'))
244
self._lock_mode = 'r'
250
if not self._lock_mode:
251
from errors import LockError
252
raise LockError('branch %r is not locked' % (self))
254
if self._lock_count > 1:
255
self._lock_count -= 1
259
self._lock_mode = self._lock_count = None
262
def abspath(self, name):
263
"""Return absolute filename for something in the branch"""
264
return os.path.join(self.base, name)
267
def relpath(self, path):
268
"""Return path relative to this branch of something inside it.
270
Raises an error if path is not in this branch."""
271
return _relpath(self.base, path)
274
def controlfilename(self, file_or_path):
275
"""Return location relative to branch."""
276
if isinstance(file_or_path, basestring):
277
file_or_path = [file_or_path]
278
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
281
def controlfile(self, file_or_path, mode='r'):
282
"""Open a control file for this branch.
284
There are two classes of file in the control directory: text
285
and binary. binary files are untranslated byte streams. Text
286
control files are stored with Unix newlines and in UTF-8, even
287
if the platform or locale defaults are different.
289
Controlfiles should almost never be opened in write mode but
290
rather should be atomically copied and replaced using atomicfile.
293
fn = self.controlfilename(file_or_path)
295
if mode == 'rb' or mode == 'wb':
296
return file(fn, mode)
297
elif mode == 'r' or mode == 'w':
298
# open in binary mode anyhow so there's no newline translation;
299
# codecs uses line buffering by default; don't want that.
301
return codecs.open(fn, mode + 'b', 'utf-8',
304
raise BzrError("invalid controlfile mode %r" % mode)
308
def _make_control(self):
309
from bzrlib.inventory import Inventory
310
from bzrlib.xml import pack_xml
312
os.mkdir(self.controlfilename([]))
313
self.controlfile('README', 'w').write(
314
"This is a Bazaar-NG control directory.\n"
315
"Do not change any files in this directory.\n")
316
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
317
for d in ('text-store', 'inventory-store', 'revision-store'):
318
os.mkdir(self.controlfilename(d))
319
for f in ('revision-history', 'merged-patches',
320
'pending-merged-patches', 'branch-name',
323
self.controlfile(f, 'w').write('')
324
mutter('created control directory in ' + self.base)
326
pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
329
def _check_format(self):
330
"""Check this branch format is supported.
332
The current tool only supports the current unstable format.
334
In the future, we might need different in-memory Branch
335
classes to support downlevel branches. But not yet.
337
# This ignores newlines so that we can open branches created
338
# on Windows from Linux and so on. I think it might be better
339
# to always make all internal files in unix format.
340
fmt = self.controlfile('branch-format', 'r').read()
341
fmt.replace('\r\n', '')
342
if fmt != BZR_BRANCH_FORMAT:
343
raise BzrError('sorry, branch format %r not supported' % fmt,
344
['use a different bzr version',
345
'or remove the .bzr directory and "bzr init" again'])
347
def get_root_id(self):
348
"""Return the id of this branches root"""
349
inv = self.read_working_inventory()
350
return inv.root.file_id
352
def set_root_id(self, file_id):
353
inv = self.read_working_inventory()
354
orig_root_id = inv.root.file_id
355
del inv._byid[inv.root.file_id]
356
inv.root.file_id = file_id
357
inv._byid[inv.root.file_id] = inv.root
360
if entry.parent_id in (None, orig_root_id):
361
entry.parent_id = inv.root.file_id
362
self._write_inventory(inv)
364
def read_working_inventory(self):
365
"""Read the working inventory."""
366
from bzrlib.inventory import Inventory
367
from bzrlib.xml import unpack_xml
368
from time import time
372
# ElementTree does its own conversion from UTF-8, so open in
374
inv = unpack_xml(Inventory,
375
self.controlfile('inventory', 'rb'))
376
mutter("loaded inventory of %d items in %f"
377
% (len(inv), time() - before))
383
def _write_inventory(self, inv):
384
"""Update the working inventory.
386
That is to say, the inventory describing changes underway, that
387
will be committed to the next revision.
389
from bzrlib.atomicfile import AtomicFile
390
from bzrlib.xml import pack_xml
394
f = AtomicFile(self.controlfilename('inventory'), 'wb')
403
mutter('wrote working inventory')
406
inventory = property(read_working_inventory, _write_inventory, None,
407
"""Inventory for the working copy.""")
410
def add(self, files, verbose=False, ids=None):
411
"""Make files versioned.
413
Note that the command line normally calls smart_add instead.
415
This puts the files in the Added state, so that they will be
416
recorded by the next commit.
419
List of paths to add, relative to the base of the tree.
422
If set, use these instead of automatically generated ids.
423
Must be the same length as the list of files, but may
424
contain None for ids that are to be autogenerated.
426
TODO: Perhaps have an option to add the ids even if the files do
429
TODO: Perhaps return the ids of the files? But then again it
430
is easy to retrieve them if they're needed.
432
TODO: Adding a directory should optionally recurse down and
433
add all non-ignored children. Perhaps do that in a
436
# TODO: Re-adding a file that is removed in the working copy
437
# should probably put it back with the previous ID.
438
if isinstance(files, basestring):
439
assert(ids is None or isinstance(ids, basestring))
445
ids = [None] * len(files)
447
assert(len(ids) == len(files))
451
inv = self.read_working_inventory()
452
for f,file_id in zip(files, ids):
453
if is_control_file(f):
454
raise BzrError("cannot add control file %s" % quotefn(f))
459
raise BzrError("cannot add top-level %r" % f)
461
fullpath = os.path.normpath(self.abspath(f))
464
kind = file_kind(fullpath)
466
# maybe something better?
467
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
469
if kind != 'file' and kind != 'directory':
470
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
473
file_id = gen_file_id(f)
474
inv.add_path(f, kind=kind, file_id=file_id)
477
print 'added', quotefn(f)
479
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
481
self._write_inventory(inv)
486
def print_file(self, file, revno):
487
"""Print `file` to stdout."""
490
tree = self.revision_tree(self.lookup_revision(revno))
491
# use inventory as it was in that revision
492
file_id = tree.inventory.path2id(file)
494
raise BzrError("%r is not present in revision %s" % (file, revno))
495
tree.print_file(file_id)
500
def remove(self, files, verbose=False):
501
"""Mark nominated files for removal from the inventory.
503
This does not remove their text. This does not run on
505
TODO: Refuse to remove modified files unless --force is given?
507
TODO: Do something useful with directories.
509
TODO: Should this remove the text or not? Tough call; not
510
removing may be useful and the user can just use use rm, and
511
is the opposite of add. Removing it is consistent with most
512
other tools. Maybe an option.
514
## TODO: Normalize names
515
## TODO: Remove nested loops; better scalability
516
if isinstance(files, basestring):
522
tree = self.working_tree()
525
# do this before any modifications
529
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
530
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
532
# having remove it, it must be either ignored or unknown
533
if tree.is_ignored(f):
537
show_status(new_status, inv[fid].kind, quotefn(f))
540
self._write_inventory(inv)
545
# FIXME: this doesn't need to be a branch method
546
def set_inventory(self, new_inventory_list):
547
from bzrlib.inventory import Inventory, InventoryEntry
548
inv = Inventory(self.get_root_id())
549
for path, file_id, parent, kind in new_inventory_list:
550
name = os.path.basename(path)
553
inv.add(InventoryEntry(file_id, name, kind, parent))
554
self._write_inventory(inv)
558
"""Return all unknown files.
560
These are files in the working directory that are not versioned or
561
control files or ignored.
563
>>> b = ScratchBranch(files=['foo', 'foo~'])
564
>>> list(b.unknowns())
567
>>> list(b.unknowns())
570
>>> list(b.unknowns())
573
return self.working_tree().unknowns()
576
def append_revision(self, *revision_ids):
577
from bzrlib.atomicfile import AtomicFile
579
for revision_id in revision_ids:
580
mutter("add {%s} to revision-history" % revision_id)
582
rev_history = self.revision_history()
583
rev_history.extend(revision_ids)
585
f = AtomicFile(self.controlfilename('revision-history'))
587
for rev_id in rev_history:
594
def get_revision(self, revision_id):
595
"""Return the Revision object for a named revision"""
598
if not revision_id or not isinstance(revision_id, basestring):
599
raise InvalidRevisionId(revision_id)
600
r = unpack_xml(Revision, self.revision_store[revision_id])
604
assert r.revision_id == revision_id
608
def get_revision_delta(self, revno):
609
"""Return the delta for one revision.
611
The delta is relative to its mainline predecessor, or the
612
empty tree for revision 1.
614
assert isinstance(revno, int)
615
rh = self.revision_history()
616
if not (1 <= revno <= len(rh)):
617
raise InvalidRevisionNumber(revno)
619
# revno is 1-based; list is 0-based
621
new_tree = self.revision_tree(rh[revno-1])
623
old_tree = EmptyTree()
625
old_tree = self.revision_tree(rh[revno-2])
627
return compare_trees(old_tree, new_tree)
631
def get_revision_sha1(self, revision_id):
632
"""Hash the stored value of a revision, and return it."""
633
# In the future, revision entries will be signed. At that
634
# point, it is probably best *not* to include the signature
635
# in the revision hash. Because that lets you re-sign
636
# the revision, (add signatures/remove signatures) and still
637
# have all hash pointers stay consistent.
638
# But for now, just hash the contents.
639
return sha_file(self.revision_store[revision_id])
642
def get_inventory(self, inventory_id):
643
"""Get Inventory object by hash.
645
TODO: Perhaps for this and similar methods, take a revision
646
parameter which can be either an integer revno or a
648
from bzrlib.inventory import Inventory
649
from bzrlib.xml import unpack_xml
651
return unpack_xml(Inventory, self.inventory_store[inventory_id])
654
def get_inventory_sha1(self, inventory_id):
655
"""Return the sha1 hash of the inventory entry
657
return sha_file(self.inventory_store[inventory_id])
660
def get_revision_inventory(self, revision_id):
661
"""Return inventory of a past revision."""
662
# bzr 0.0.6 imposes the constraint that the inventory_id
663
# must be the same as its revision, so this is trivial.
664
if revision_id == None:
665
from bzrlib.inventory import Inventory
666
return Inventory(self.get_root_id())
668
return self.get_inventory(revision_id)
671
def revision_history(self):
672
"""Return sequence of revision hashes on to this branch.
674
>>> ScratchBranch().revision_history()
679
return [l.rstrip('\r\n') for l in
680
self.controlfile('revision-history', 'r').readlines()]
685
def common_ancestor(self, other, self_revno=None, other_revno=None):
688
>>> sb = ScratchBranch(files=['foo', 'foo~'])
689
>>> sb.common_ancestor(sb) == (None, None)
691
>>> commit.commit(sb, "Committing first revision", verbose=False)
692
>>> sb.common_ancestor(sb)[0]
694
>>> clone = sb.clone()
695
>>> commit.commit(sb, "Committing second revision", verbose=False)
696
>>> sb.common_ancestor(sb)[0]
698
>>> sb.common_ancestor(clone)[0]
700
>>> commit.commit(clone, "Committing divergent second revision",
702
>>> sb.common_ancestor(clone)[0]
704
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
706
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
708
>>> clone2 = sb.clone()
709
>>> sb.common_ancestor(clone2)[0]
711
>>> sb.common_ancestor(clone2, self_revno=1)[0]
713
>>> sb.common_ancestor(clone2, other_revno=1)[0]
716
my_history = self.revision_history()
717
other_history = other.revision_history()
718
if self_revno is None:
719
self_revno = len(my_history)
720
if other_revno is None:
721
other_revno = len(other_history)
722
indices = range(min((self_revno, other_revno)))
725
if my_history[r] == other_history[r]:
726
return r+1, my_history[r]
731
"""Return current revision number for this branch.
733
That is equivalent to the number of revisions committed to
736
return len(self.revision_history())
739
def last_patch(self):
740
"""Return last patch hash, or None if no history.
742
ph = self.revision_history()
749
def missing_revisions(self, other, stop_revision=None):
751
If self and other have not diverged, return a list of the revisions
752
present in other, but missing from self.
754
>>> from bzrlib.commit import commit
755
>>> bzrlib.trace.silent = True
756
>>> br1 = ScratchBranch()
757
>>> br2 = ScratchBranch()
758
>>> br1.missing_revisions(br2)
760
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
761
>>> br1.missing_revisions(br2)
763
>>> br2.missing_revisions(br1)
765
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
766
>>> br1.missing_revisions(br2)
768
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
769
>>> br1.missing_revisions(br2)
771
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
772
>>> br1.missing_revisions(br2)
773
Traceback (most recent call last):
774
DivergedBranches: These branches have diverged.
776
self_history = self.revision_history()
777
self_len = len(self_history)
778
other_history = other.revision_history()
779
other_len = len(other_history)
780
common_index = min(self_len, other_len) -1
781
if common_index >= 0 and \
782
self_history[common_index] != other_history[common_index]:
783
raise DivergedBranches(self, other)
785
if stop_revision is None:
786
stop_revision = other_len
787
elif stop_revision > other_len:
788
raise NoSuchRevision(self, stop_revision)
790
return other_history[self_len:stop_revision]
793
def update_revisions(self, other, stop_revision=None):
794
"""Pull in all new revisions from other branch.
796
>>> from bzrlib.commit import commit
797
>>> bzrlib.trace.silent = True
798
>>> br1 = ScratchBranch(files=['foo', 'bar'])
801
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
802
>>> br2 = ScratchBranch()
803
>>> br2.update_revisions(br1)
807
>>> br2.revision_history()
809
>>> br2.update_revisions(br1)
813
>>> br1.text_store.total_size() == br2.text_store.total_size()
816
from bzrlib.progress import ProgressBar
820
pb.update('comparing histories')
821
revision_ids = self.missing_revisions(other, stop_revision)
823
if hasattr(other.revision_store, "prefetch"):
824
other.revision_store.prefetch(revision_ids)
825
if hasattr(other.inventory_store, "prefetch"):
826
inventory_ids = [other.get_revision(r).inventory_id
827
for r in revision_ids]
828
other.inventory_store.prefetch(inventory_ids)
833
for rev_id in revision_ids:
835
pb.update('fetching revision', i, len(revision_ids))
836
rev = other.get_revision(rev_id)
837
revisions.append(rev)
838
inv = other.get_inventory(str(rev.inventory_id))
839
for key, entry in inv.iter_entries():
840
if entry.text_id is None:
842
if entry.text_id not in self.text_store:
843
needed_texts.add(entry.text_id)
847
count = self.text_store.copy_multi(other.text_store, needed_texts)
848
print "Added %d texts." % count
849
inventory_ids = [ f.inventory_id for f in revisions ]
850
count = self.inventory_store.copy_multi(other.inventory_store,
852
print "Added %d inventories." % count
853
revision_ids = [ f.revision_id for f in revisions]
854
count = self.revision_store.copy_multi(other.revision_store,
856
for revision_id in revision_ids:
857
self.append_revision(revision_id)
858
print "Added %d revisions." % count
861
def commit(self, *args, **kw):
862
from bzrlib.commit import commit
863
commit(self, *args, **kw)
866
def lookup_revision(self, revision):
867
"""Return the revision identifier for a given revision information."""
868
revno, info = self.get_revision_info(revision)
871
def get_revision_info(self, revision):
872
"""Return (revno, revision id) for revision identifier.
874
revision can be an integer, in which case it is assumed to be revno (though
875
this will translate negative values into positive ones)
876
revision can also be a string, in which case it is parsed for something like
877
'date:' or 'revid:' etc.
882
try:# Convert to int if possible
883
revision = int(revision)
886
revs = self.revision_history()
887
if isinstance(revision, int):
890
# Mabye we should do this first, but we don't need it if revision == 0
892
revno = len(revs) + revision + 1
895
elif isinstance(revision, basestring):
896
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
897
if revision.startswith(prefix):
898
revno = func(self, revs, revision)
901
raise BzrError('No namespace registered for string: %r' % revision)
903
if revno is None or revno <= 0 or revno > len(revs):
904
raise BzrError("no such revision %s" % revision)
905
return revno, revs[revno-1]
907
def _namespace_revno(self, revs, revision):
908
"""Lookup a revision by revision number"""
909
assert revision.startswith('revno:')
911
return int(revision[6:])
914
REVISION_NAMESPACES['revno:'] = _namespace_revno
916
def _namespace_revid(self, revs, revision):
917
assert revision.startswith('revid:')
919
return revs.index(revision[6:]) + 1
922
REVISION_NAMESPACES['revid:'] = _namespace_revid
924
def _namespace_last(self, revs, revision):
925
assert revision.startswith('last:')
927
offset = int(revision[5:])
932
raise BzrError('You must supply a positive value for --revision last:XXX')
933
return len(revs) - offset + 1
934
REVISION_NAMESPACES['last:'] = _namespace_last
936
def _namespace_tag(self, revs, revision):
937
assert revision.startswith('tag:')
938
raise BzrError('tag: namespace registered, but not implemented.')
939
REVISION_NAMESPACES['tag:'] = _namespace_tag
941
def _namespace_date(self, revs, revision):
942
assert revision.startswith('date:')
944
# Spec for date revisions:
946
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
947
# it can also start with a '+/-/='. '+' says match the first
948
# entry after the given date. '-' is match the first entry before the date
949
# '=' is match the first entry after, but still on the given date.
951
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
952
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
953
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
954
# May 13th, 2005 at 0:00
956
# So the proper way of saying 'give me all entries for today' is:
957
# -r {date:+today}:{date:-tomorrow}
958
# The default is '=' when not supplied
961
if val[:1] in ('+', '-', '='):
962
match_style = val[:1]
965
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
966
if val.lower() == 'yesterday':
967
dt = today - datetime.timedelta(days=1)
968
elif val.lower() == 'today':
970
elif val.lower() == 'tomorrow':
971
dt = today + datetime.timedelta(days=1)
974
# This should be done outside the function to avoid recompiling it.
975
_date_re = re.compile(
976
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
978
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
980
m = _date_re.match(val)
981
if not m or (not m.group('date') and not m.group('time')):
982
raise BzrError('Invalid revision date %r' % revision)
985
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
987
year, month, day = today.year, today.month, today.day
989
hour = int(m.group('hour'))
990
minute = int(m.group('minute'))
991
if m.group('second'):
992
second = int(m.group('second'))
996
hour, minute, second = 0,0,0
998
dt = datetime.datetime(year=year, month=month, day=day,
999
hour=hour, minute=minute, second=second)
1003
if match_style == '-':
1005
elif match_style == '=':
1006
last = dt + datetime.timedelta(days=1)
1009
for i in range(len(revs)-1, -1, -1):
1010
r = self.get_revision(revs[i])
1011
# TODO: Handle timezone.
1012
dt = datetime.datetime.fromtimestamp(r.timestamp)
1013
if first >= dt and (last is None or dt >= last):
1016
for i in range(len(revs)):
1017
r = self.get_revision(revs[i])
1018
# TODO: Handle timezone.
1019
dt = datetime.datetime.fromtimestamp(r.timestamp)
1020
if first <= dt and (last is None or dt <= last):
1022
REVISION_NAMESPACES['date:'] = _namespace_date
1024
def revision_tree(self, revision_id):
1025
"""Return Tree for a revision on this branch.
1027
`revision_id` may be None for the null revision, in which case
1028
an `EmptyTree` is returned."""
1029
# TODO: refactor this to use an existing revision object
1030
# so we don't need to read it in twice.
1031
if revision_id == None:
1034
inv = self.get_revision_inventory(revision_id)
1035
return RevisionTree(self.text_store, inv)
1038
def working_tree(self):
1039
"""Return a `Tree` for the working copy."""
1040
from workingtree import WorkingTree
1041
return WorkingTree(self.base, self.read_working_inventory())
1044
def basis_tree(self):
1045
"""Return `Tree` object for last revision.
1047
If there are no revisions yet, return an `EmptyTree`.
1049
r = self.last_patch()
1053
return RevisionTree(self.text_store, self.get_revision_inventory(r))
1057
def rename_one(self, from_rel, to_rel):
1060
This can change the directory or the filename or both.
1064
tree = self.working_tree()
1065
inv = tree.inventory
1066
if not tree.has_filename(from_rel):
1067
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1068
if tree.has_filename(to_rel):
1069
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1071
file_id = inv.path2id(from_rel)
1073
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1075
if inv.path2id(to_rel):
1076
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1078
to_dir, to_tail = os.path.split(to_rel)
1079
to_dir_id = inv.path2id(to_dir)
1080
if to_dir_id == None and to_dir != '':
1081
raise BzrError("can't determine destination directory id for %r" % to_dir)
1083
mutter("rename_one:")
1084
mutter(" file_id {%s}" % file_id)
1085
mutter(" from_rel %r" % from_rel)
1086
mutter(" to_rel %r" % to_rel)
1087
mutter(" to_dir %r" % to_dir)
1088
mutter(" to_dir_id {%s}" % to_dir_id)
1090
inv.rename(file_id, to_dir_id, to_tail)
1092
print "%s => %s" % (from_rel, to_rel)
1094
from_abs = self.abspath(from_rel)
1095
to_abs = self.abspath(to_rel)
1097
os.rename(from_abs, to_abs)
1099
raise BzrError("failed to rename %r to %r: %s"
1100
% (from_abs, to_abs, e[1]),
1101
["rename rolled back"])
1103
self._write_inventory(inv)
1108
def move(self, from_paths, to_name):
1111
to_name must exist as a versioned directory.
1113
If to_name exists and is a directory, the files are moved into
1114
it, keeping their old names. If it is a directory,
1116
Note that to_name is only the last component of the new name;
1117
this doesn't change the directory.
1121
## TODO: Option to move IDs only
1122
assert not isinstance(from_paths, basestring)
1123
tree = self.working_tree()
1124
inv = tree.inventory
1125
to_abs = self.abspath(to_name)
1126
if not isdir(to_abs):
1127
raise BzrError("destination %r is not a directory" % to_abs)
1128
if not tree.has_filename(to_name):
1129
raise BzrError("destination %r not in working directory" % to_abs)
1130
to_dir_id = inv.path2id(to_name)
1131
if to_dir_id == None and to_name != '':
1132
raise BzrError("destination %r is not a versioned directory" % to_name)
1133
to_dir_ie = inv[to_dir_id]
1134
if to_dir_ie.kind not in ('directory', 'root_directory'):
1135
raise BzrError("destination %r is not a directory" % to_abs)
1137
to_idpath = inv.get_idpath(to_dir_id)
1139
for f in from_paths:
1140
if not tree.has_filename(f):
1141
raise BzrError("%r does not exist in working tree" % f)
1142
f_id = inv.path2id(f)
1144
raise BzrError("%r is not versioned" % f)
1145
name_tail = splitpath(f)[-1]
1146
dest_path = appendpath(to_name, name_tail)
1147
if tree.has_filename(dest_path):
1148
raise BzrError("destination %r already exists" % dest_path)
1149
if f_id in to_idpath:
1150
raise BzrError("can't move %r to a subdirectory of itself" % f)
1152
# OK, so there's a race here, it's possible that someone will
1153
# create a file in this interval and then the rename might be
1154
# left half-done. But we should have caught most problems.
1156
for f in from_paths:
1157
name_tail = splitpath(f)[-1]
1158
dest_path = appendpath(to_name, name_tail)
1159
print "%s => %s" % (f, dest_path)
1160
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1162
os.rename(self.abspath(f), self.abspath(dest_path))
1164
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1165
["rename rolled back"])
1167
self._write_inventory(inv)
1172
def revert(self, filenames, old_tree=None, backups=True):
1173
"""Restore selected files to the versions from a previous tree.
1176
If true (default) backups are made of files before
1179
from bzrlib.errors import NotVersionedError, BzrError
1180
from bzrlib.atomicfile import AtomicFile
1181
from bzrlib.osutils import backup_file
1183
inv = self.read_working_inventory()
1184
if old_tree is None:
1185
old_tree = self.basis_tree()
1186
old_inv = old_tree.inventory
1189
for fn in filenames:
1190
file_id = inv.path2id(fn)
1192
raise NotVersionedError("not a versioned file", fn)
1193
if not old_inv.has_id(file_id):
1194
raise BzrError("file not present in old tree", fn, file_id)
1195
nids.append((fn, file_id))
1197
# TODO: Rename back if it was previously at a different location
1199
# TODO: If given a directory, restore the entire contents from
1200
# the previous version.
1202
# TODO: Make a backup to a temporary file.
1204
# TODO: If the file previously didn't exist, delete it?
1205
for fn, file_id in nids:
1208
f = AtomicFile(fn, 'wb')
1210
f.write(old_tree.get_file(file_id).read())
1216
def pending_merges(self):
1217
"""Return a list of pending merges.
1219
These are revisions that have been merged into the working
1220
directory but not yet committed.
1222
cfn = self.controlfilename('pending-merges')
1223
if not os.path.exists(cfn):
1226
for l in self.controlfile('pending-merges', 'r').readlines():
1227
p.append(l.rstrip('\n'))
1231
def add_pending_merge(self, revision_id):
1232
from bzrlib.revision import validate_revision_id
1234
validate_revision_id(revision_id)
1236
p = self.pending_merges()
1237
if revision_id in p:
1239
p.append(revision_id)
1240
self.set_pending_merges(p)
1243
def set_pending_merges(self, rev_list):
1244
from bzrlib.atomicfile import AtomicFile
1247
f = AtomicFile(self.controlfilename('pending-merges'))
1259
class ScratchBranch(Branch):
1260
"""Special test class: a branch that cleans up after itself.
1262
>>> b = ScratchBranch()
1270
def __init__(self, files=[], dirs=[], base=None):
1271
"""Make a test branch.
1273
This creates a temporary directory and runs init-tree in it.
1275
If any files are listed, they are created in the working copy.
1277
from tempfile import mkdtemp
1282
Branch.__init__(self, base, init=init)
1284
os.mkdir(self.abspath(d))
1287
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1292
>>> orig = ScratchBranch(files=["file1", "file2"])
1293
>>> clone = orig.clone()
1294
>>> os.path.samefile(orig.base, clone.base)
1296
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1299
from shutil import copytree
1300
from tempfile import mkdtemp
1303
copytree(self.base, base, symlinks=True)
1304
return ScratchBranch(base=base)
1310
"""Destroy the test branch, removing the scratch directory."""
1311
from shutil import rmtree
1314
mutter("delete ScratchBranch %s" % self.base)
1317
# Work around for shutil.rmtree failing on Windows when
1318
# readonly files are encountered
1319
mutter("hit exception in destroying ScratchBranch: %s" % e)
1320
for root, dirs, files in os.walk(self.base, topdown=False):
1322
os.chmod(os.path.join(root, name), 0700)
1328
######################################################################
1332
def is_control_file(filename):
1333
## FIXME: better check
1334
filename = os.path.normpath(filename)
1335
while filename != '':
1336
head, tail = os.path.split(filename)
1337
## mutter('check %r for control file' % ((head, tail), ))
1338
if tail == bzrlib.BZRDIR:
1340
if filename == head:
1347
def gen_file_id(name):
1348
"""Return new file id.
1350
This should probably generate proper UUIDs, but for the moment we
1351
cope with just randomness because running uuidgen every time is
1354
from binascii import hexlify
1355
from time import time
1357
# get last component
1358
idx = name.rfind('/')
1360
name = name[idx+1 : ]
1361
idx = name.rfind('\\')
1363
name = name[idx+1 : ]
1365
# make it not a hidden file
1366
name = name.lstrip('.')
1368
# remove any wierd characters; we don't escape them but rather
1369
# just pull them out
1370
name = re.sub(r'[^\w.]', '', name)
1372
s = hexlify(rand_bytes(8))
1373
return '-'.join((name, compact_date(time()), s))
1377
"""Return a new tree-root file id."""
1378
return gen_file_id('TREE_ROOT')