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
21
from bzrlib.trace import mutter, note
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
23
sha_file, appendpath, file_kind
24
from bzrlib.errors import BzrError
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
## TODO: Maybe include checks for common corruption of newlines, etc?
31
def find_branch(f, **args):
32
if f and (f.startswith('http://') or f.startswith('https://')):
34
return remotebranch.RemoteBranch(f, **args)
36
return Branch(f, **args)
39
def find_cached_branch(f, cache_root, **args):
40
from remotebranch import RemoteBranch
41
br = find_branch(f, **args)
42
def cacheify(br, store_name):
43
from meta_store import CachedStore
44
cache_path = os.path.join(cache_root, store_name)
46
new_store = CachedStore(getattr(br, store_name), cache_path)
47
setattr(br, store_name, new_store)
49
if isinstance(br, RemoteBranch):
50
cacheify(br, 'inventory_store')
51
cacheify(br, 'text_store')
52
cacheify(br, 'revision_store')
56
def _relpath(base, path):
57
"""Return path relative to base, or raise exception.
59
The path may be either an absolute path or a path relative to the
60
current working directory.
62
Lifted out of Branch.relpath for ease of testing.
64
os.path.commonprefix (python2.4) has a bad bug that it works just
65
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
66
avoids that problem."""
67
rp = os.path.abspath(path)
71
while len(head) >= len(base):
74
head, tail = os.path.split(head)
78
from errors import NotBranchError
79
raise NotBranchError("path %r is not within branch %r" % (rp, base))
84
def find_branch_root(f=None):
85
"""Find the branch root enclosing f, or pwd.
87
f may be a filename or a URL.
89
It is not necessary that f exists.
91
Basically we keep looking up until we find the control directory or
95
elif hasattr(os.path, 'realpath'):
96
f = os.path.realpath(f)
98
f = os.path.abspath(f)
99
if not os.path.exists(f):
100
raise BzrError('%r does not exist' % f)
106
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
108
head, tail = os.path.split(f)
110
# reached the root, whatever that may be
111
raise BzrError('%r is not in a branch' % orig_f)
114
class DivergedBranches(Exception):
115
def __init__(self, branch1, branch2):
116
self.branch1 = branch1
117
self.branch2 = branch2
118
Exception.__init__(self, "These branches have diverged.")
121
class NoSuchRevision(BzrError):
122
def __init__(self, branch, revision):
124
self.revision = revision
125
msg = "Branch %s has no revision %d" % (branch, revision)
126
BzrError.__init__(self, msg)
129
######################################################################
132
class Branch(object):
133
"""Branch holding a history of revisions.
136
Base directory of the branch.
142
If _lock_mode is true, a positive count of the number of times the
146
Lock object from bzrlib.lock.
153
# Map some sort of prefix into a namespace
154
# stuff like "revno:10", "revid:", etc.
155
# This should match a prefix with a function which accepts
156
REVISION_NAMESPACES = {}
158
def __init__(self, base, init=False, find_root=True):
159
"""Create new branch object at a particular location.
161
base -- Base directory for the branch.
163
init -- If True, create new control files in a previously
164
unversioned directory. If False, the branch must already
167
find_root -- If true and init is false, find the root of the
168
existing branch containing base.
170
In the test suite, creation of new trees is tested using the
171
`ScratchBranch` class.
173
from bzrlib.store import ImmutableStore
175
self.base = os.path.realpath(base)
178
self.base = find_branch_root(base)
180
self.base = os.path.realpath(base)
181
if not isdir(self.controlfilename('.')):
182
from errors import NotBranchError
183
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
184
['use "bzr init" to initialize a new working tree',
185
'current bzr can only operate from top-of-tree'])
188
self.text_store = ImmutableStore(self.controlfilename('text-store'))
189
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
190
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
194
return '%s(%r)' % (self.__class__.__name__, self.base)
201
if self._lock_mode or self._lock:
202
from warnings import warn
203
warn("branch %r was not explicitly unlocked" % self)
208
def lock_write(self):
210
if self._lock_mode != 'w':
211
from errors import LockError
212
raise LockError("can't upgrade to a write lock from %r" %
214
self._lock_count += 1
216
from bzrlib.lock import WriteLock
218
self._lock = WriteLock(self.controlfilename('branch-lock'))
219
self._lock_mode = 'w'
226
assert self._lock_mode in ('r', 'w'), \
227
"invalid lock mode %r" % self._lock_mode
228
self._lock_count += 1
230
from bzrlib.lock import ReadLock
232
self._lock = ReadLock(self.controlfilename('branch-lock'))
233
self._lock_mode = 'r'
239
if not self._lock_mode:
240
from errors import LockError
241
raise LockError('branch %r is not locked' % (self))
243
if self._lock_count > 1:
244
self._lock_count -= 1
248
self._lock_mode = self._lock_count = None
251
def abspath(self, name):
252
"""Return absolute filename for something in the branch"""
253
return os.path.join(self.base, name)
256
def relpath(self, path):
257
"""Return path relative to this branch of something inside it.
259
Raises an error if path is not in this branch."""
260
return _relpath(self.base, path)
263
def controlfilename(self, file_or_path):
264
"""Return location relative to branch."""
265
if isinstance(file_or_path, basestring):
266
file_or_path = [file_or_path]
267
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
270
def controlfile(self, file_or_path, mode='r'):
271
"""Open a control file for this branch.
273
There are two classes of file in the control directory: text
274
and binary. binary files are untranslated byte streams. Text
275
control files are stored with Unix newlines and in UTF-8, even
276
if the platform or locale defaults are different.
278
Controlfiles should almost never be opened in write mode but
279
rather should be atomically copied and replaced using atomicfile.
282
fn = self.controlfilename(file_or_path)
284
if mode == 'rb' or mode == 'wb':
285
return file(fn, mode)
286
elif mode == 'r' or mode == 'w':
287
# open in binary mode anyhow so there's no newline translation;
288
# codecs uses line buffering by default; don't want that.
290
return codecs.open(fn, mode + 'b', 'utf-8',
293
raise BzrError("invalid controlfile mode %r" % mode)
297
def _make_control(self):
298
from bzrlib.inventory import Inventory
299
from bzrlib.xml import pack_xml
301
os.mkdir(self.controlfilename([]))
302
self.controlfile('README', 'w').write(
303
"This is a Bazaar-NG control directory.\n"
304
"Do not change any files in this directory.\n")
305
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
306
for d in ('text-store', 'inventory-store', 'revision-store'):
307
os.mkdir(self.controlfilename(d))
308
for f in ('revision-history', 'merged-patches',
309
'pending-merged-patches', 'branch-name',
312
self.controlfile(f, 'w').write('')
313
mutter('created control directory in ' + self.base)
315
pack_xml(Inventory(), self.controlfile('inventory','w'))
318
def _check_format(self):
319
"""Check this branch format is supported.
321
The current tool only supports the current unstable format.
323
In the future, we might need different in-memory Branch
324
classes to support downlevel branches. But not yet.
326
# This ignores newlines so that we can open branches created
327
# on Windows from Linux and so on. I think it might be better
328
# to always make all internal files in unix format.
329
fmt = self.controlfile('branch-format', 'r').read()
330
fmt.replace('\r\n', '')
331
if fmt != BZR_BRANCH_FORMAT:
332
raise BzrError('sorry, branch format %r not supported' % fmt,
333
['use a different bzr version',
334
'or remove the .bzr directory and "bzr init" again'])
338
def read_working_inventory(self):
339
"""Read the working inventory."""
340
from bzrlib.inventory import Inventory
341
from bzrlib.xml import unpack_xml
342
from time import time
346
# ElementTree does its own conversion from UTF-8, so open in
348
inv = unpack_xml(Inventory,
349
self.controlfile('inventory', 'rb'))
350
mutter("loaded inventory of %d items in %f"
351
% (len(inv), time() - before))
357
def _write_inventory(self, inv):
358
"""Update the working inventory.
360
That is to say, the inventory describing changes underway, that
361
will be committed to the next revision.
363
from bzrlib.atomicfile import AtomicFile
364
from bzrlib.xml import pack_xml
368
f = AtomicFile(self.controlfilename('inventory'), 'wb')
377
mutter('wrote working inventory')
380
inventory = property(read_working_inventory, _write_inventory, None,
381
"""Inventory for the working copy.""")
384
def add(self, files, verbose=False, ids=None):
385
"""Make files versioned.
387
Note that the command line normally calls smart_add instead.
389
This puts the files in the Added state, so that they will be
390
recorded by the next commit.
393
List of paths to add, relative to the base of the tree.
396
If set, use these instead of automatically generated ids.
397
Must be the same length as the list of files, but may
398
contain None for ids that are to be autogenerated.
400
TODO: Perhaps have an option to add the ids even if the files do
403
TODO: Perhaps return the ids of the files? But then again it
404
is easy to retrieve them if they're needed.
406
TODO: Adding a directory should optionally recurse down and
407
add all non-ignored children. Perhaps do that in a
410
from bzrlib.textui import show_status
411
# TODO: Re-adding a file that is removed in the working copy
412
# should probably put it back with the previous ID.
413
if isinstance(files, basestring):
414
assert(ids is None or isinstance(ids, basestring))
420
ids = [None] * len(files)
422
assert(len(ids) == len(files))
426
inv = self.read_working_inventory()
427
for f,file_id in zip(files, ids):
428
if is_control_file(f):
429
raise BzrError("cannot add control file %s" % quotefn(f))
434
raise BzrError("cannot add top-level %r" % f)
436
fullpath = os.path.normpath(self.abspath(f))
439
kind = file_kind(fullpath)
441
# maybe something better?
442
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
444
if kind != 'file' and kind != 'directory':
445
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
448
file_id = gen_file_id(f)
449
inv.add_path(f, kind=kind, file_id=file_id)
452
print 'added', quotefn(f)
454
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
456
self._write_inventory(inv)
461
def print_file(self, file, revno):
462
"""Print `file` to stdout."""
465
tree = self.revision_tree(self.lookup_revision(revno))
466
# use inventory as it was in that revision
467
file_id = tree.inventory.path2id(file)
469
raise BzrError("%r is not present in revision %s" % (file, revno))
470
tree.print_file(file_id)
475
def remove(self, files, verbose=False):
476
"""Mark nominated files for removal from the inventory.
478
This does not remove their text. This does not run on
480
TODO: Refuse to remove modified files unless --force is given?
482
TODO: Do something useful with directories.
484
TODO: Should this remove the text or not? Tough call; not
485
removing may be useful and the user can just use use rm, and
486
is the opposite of add. Removing it is consistent with most
487
other tools. Maybe an option.
489
from bzrlib.textui import show_status
490
## TODO: Normalize names
491
## TODO: Remove nested loops; better scalability
492
if isinstance(files, basestring):
498
tree = self.working_tree()
501
# do this before any modifications
505
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
506
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
508
# having remove it, it must be either ignored or unknown
509
if tree.is_ignored(f):
513
show_status(new_status, inv[fid].kind, quotefn(f))
516
self._write_inventory(inv)
521
# FIXME: this doesn't need to be a branch method
522
def set_inventory(self, new_inventory_list):
523
from bzrlib.inventory import Inventory, InventoryEntry
525
for path, file_id, parent, kind in new_inventory_list:
526
name = os.path.basename(path)
529
inv.add(InventoryEntry(file_id, name, kind, parent))
530
self._write_inventory(inv)
534
"""Return all unknown files.
536
These are files in the working directory that are not versioned or
537
control files or ignored.
539
>>> b = ScratchBranch(files=['foo', 'foo~'])
540
>>> list(b.unknowns())
543
>>> list(b.unknowns())
546
>>> list(b.unknowns())
549
return self.working_tree().unknowns()
552
def append_revision(self, revision_id):
553
from bzrlib.atomicfile import AtomicFile
555
mutter("add {%s} to revision-history" % revision_id)
556
rev_history = self.revision_history() + [revision_id]
558
f = AtomicFile(self.controlfilename('revision-history'))
560
for rev_id in rev_history:
567
def get_revision(self, revision_id):
568
"""Return the Revision object for a named revision"""
569
from bzrlib.revision import Revision
570
from bzrlib.xml import unpack_xml
574
if not revision_id or not isinstance(revision_id, basestring):
575
raise ValueError('invalid revision-id: %r' % revision_id)
576
r = unpack_xml(Revision, self.revision_store[revision_id])
580
assert r.revision_id == revision_id
584
def get_revision_sha1(self, revision_id):
585
"""Hash the stored value of a revision, and return it."""
586
# In the future, revision entries will be signed. At that
587
# point, it is probably best *not* to include the signature
588
# in the revision hash. Because that lets you re-sign
589
# the revision, (add signatures/remove signatures) and still
590
# have all hash pointers stay consistent.
591
# But for now, just hash the contents.
592
return sha_file(self.revision_store[revision_id])
595
def get_inventory(self, inventory_id):
596
"""Get Inventory object by hash.
598
TODO: Perhaps for this and similar methods, take a revision
599
parameter which can be either an integer revno or a
601
from bzrlib.inventory import Inventory
602
from bzrlib.xml import unpack_xml
604
return unpack_xml(Inventory, self.inventory_store[inventory_id])
607
def get_inventory_sha1(self, inventory_id):
608
"""Return the sha1 hash of the inventory entry
610
return sha_file(self.inventory_store[inventory_id])
613
def get_revision_inventory(self, revision_id):
614
"""Return inventory of a past revision."""
615
# bzr 0.0.6 imposes the constraint that the inventory_id
616
# must be the same as its revision, so this is trivial.
617
if revision_id == None:
618
from bzrlib.inventory import Inventory
621
return self.get_inventory(revision_id)
624
def revision_history(self):
625
"""Return sequence of revision hashes on to this branch.
627
>>> ScratchBranch().revision_history()
632
return [l.rstrip('\r\n') for l in
633
self.controlfile('revision-history', 'r').readlines()]
638
def common_ancestor(self, other, self_revno=None, other_revno=None):
641
>>> sb = ScratchBranch(files=['foo', 'foo~'])
642
>>> sb.common_ancestor(sb) == (None, None)
644
>>> commit.commit(sb, "Committing first revision", verbose=False)
645
>>> sb.common_ancestor(sb)[0]
647
>>> clone = sb.clone()
648
>>> commit.commit(sb, "Committing second revision", verbose=False)
649
>>> sb.common_ancestor(sb)[0]
651
>>> sb.common_ancestor(clone)[0]
653
>>> commit.commit(clone, "Committing divergent second revision",
655
>>> sb.common_ancestor(clone)[0]
657
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
659
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
661
>>> clone2 = sb.clone()
662
>>> sb.common_ancestor(clone2)[0]
664
>>> sb.common_ancestor(clone2, self_revno=1)[0]
666
>>> sb.common_ancestor(clone2, other_revno=1)[0]
669
my_history = self.revision_history()
670
other_history = other.revision_history()
671
if self_revno is None:
672
self_revno = len(my_history)
673
if other_revno is None:
674
other_revno = len(other_history)
675
indices = range(min((self_revno, other_revno)))
678
if my_history[r] == other_history[r]:
679
return r+1, my_history[r]
682
def enum_history(self, direction):
683
"""Return (revno, revision_id) for history of branch.
686
'forward' is from earliest to latest
687
'reverse' is from latest to earliest
689
rh = self.revision_history()
690
if direction == 'forward':
695
elif direction == 'reverse':
701
raise ValueError('invalid history direction', direction)
705
"""Return current revision number for this branch.
707
That is equivalent to the number of revisions committed to
710
return len(self.revision_history())
713
def last_patch(self):
714
"""Return last patch hash, or None if no history.
716
ph = self.revision_history()
723
def missing_revisions(self, other, stop_revision=None):
725
If self and other have not diverged, return a list of the revisions
726
present in other, but missing from self.
728
>>> from bzrlib.commit import commit
729
>>> bzrlib.trace.silent = True
730
>>> br1 = ScratchBranch()
731
>>> br2 = ScratchBranch()
732
>>> br1.missing_revisions(br2)
734
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
735
>>> br1.missing_revisions(br2)
737
>>> br2.missing_revisions(br1)
739
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
740
>>> br1.missing_revisions(br2)
742
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
743
>>> br1.missing_revisions(br2)
745
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
746
>>> br1.missing_revisions(br2)
747
Traceback (most recent call last):
748
DivergedBranches: These branches have diverged.
750
self_history = self.revision_history()
751
self_len = len(self_history)
752
other_history = other.revision_history()
753
other_len = len(other_history)
754
common_index = min(self_len, other_len) -1
755
if common_index >= 0 and \
756
self_history[common_index] != other_history[common_index]:
757
raise DivergedBranches(self, other)
759
if stop_revision is None:
760
stop_revision = other_len
761
elif stop_revision > other_len:
762
raise NoSuchRevision(self, stop_revision)
764
return other_history[self_len:stop_revision]
767
def update_revisions(self, other, stop_revision=None):
768
"""Pull in all new revisions from other branch.
770
>>> from bzrlib.commit import commit
771
>>> bzrlib.trace.silent = True
772
>>> br1 = ScratchBranch(files=['foo', 'bar'])
775
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
776
>>> br2 = ScratchBranch()
777
>>> br2.update_revisions(br1)
781
>>> br2.revision_history()
783
>>> br2.update_revisions(br1)
787
>>> br1.text_store.total_size() == br2.text_store.total_size()
790
from bzrlib.progress import ProgressBar
794
from sets import Set as set
798
pb.update('comparing histories')
799
revision_ids = self.missing_revisions(other, stop_revision)
801
if hasattr(other.revision_store, "prefetch"):
802
other.revision_store.prefetch(revision_ids)
803
if hasattr(other.inventory_store, "prefetch"):
804
inventory_ids = [other.get_revision(r).inventory_id
805
for r in revision_ids]
806
other.inventory_store.prefetch(inventory_ids)
811
for rev_id in revision_ids:
813
pb.update('fetching revision', i, len(revision_ids))
814
rev = other.get_revision(rev_id)
815
revisions.append(rev)
816
inv = other.get_inventory(str(rev.inventory_id))
817
for key, entry in inv.iter_entries():
818
if entry.text_id is None:
820
if entry.text_id not in self.text_store:
821
needed_texts.add(entry.text_id)
825
count = self.text_store.copy_multi(other.text_store, needed_texts)
826
print "Added %d texts." % count
827
inventory_ids = [ f.inventory_id for f in revisions ]
828
count = self.inventory_store.copy_multi(other.inventory_store,
830
print "Added %d inventories." % count
831
revision_ids = [ f.revision_id for f in revisions]
832
count = self.revision_store.copy_multi(other.revision_store,
834
for revision_id in revision_ids:
835
self.append_revision(revision_id)
836
print "Added %d revisions." % count
839
def commit(self, *args, **kw):
840
from bzrlib.commit import commit
841
commit(self, *args, **kw)
844
def lookup_revision(self, revision):
845
"""Return the revision identifier for a given revision information."""
846
revno, info = self.get_revision_info(revision)
849
def get_revision_info(self, revision):
850
"""Return (revno, revision id) for revision identifier.
852
revision can be an integer, in which case it is assumed to be revno (though
853
this will translate negative values into positive ones)
854
revision can also be a string, in which case it is parsed for something like
855
'date:' or 'revid:' etc.
860
try:# Convert to int if possible
861
revision = int(revision)
864
revs = self.revision_history()
865
if isinstance(revision, int):
868
# Mabye we should do this first, but we don't need it if revision == 0
870
revno = len(revs) + revision + 1
873
elif isinstance(revision, basestring):
874
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
875
if revision.startswith(prefix):
876
revno = func(self, revs, revision)
879
raise BzrError('No namespace registered for string: %r' % revision)
881
if revno is None or revno <= 0 or revno > len(revs):
882
raise BzrError("no such revision %s" % revision)
883
return revno, revs[revno-1]
885
def _namespace_revno(self, revs, revision):
886
"""Lookup a revision by revision number"""
887
assert revision.startswith('revno:')
889
return int(revision[6:])
892
REVISION_NAMESPACES['revno:'] = _namespace_revno
894
def _namespace_revid(self, revs, revision):
895
assert revision.startswith('revid:')
897
return revs.index(revision[6:]) + 1
900
REVISION_NAMESPACES['revid:'] = _namespace_revid
902
def _namespace_last(self, revs, revision):
903
assert revision.startswith('last:')
905
offset = int(revision[5:])
910
raise BzrError('You must supply a positive value for --revision last:XXX')
911
return len(revs) - offset + 1
912
REVISION_NAMESPACES['last:'] = _namespace_last
914
def _namespace_tag(self, revs, revision):
915
assert revision.startswith('tag:')
916
raise BzrError('tag: namespace registered, but not implemented.')
917
REVISION_NAMESPACES['tag:'] = _namespace_tag
919
def _namespace_date(self, revs, revision):
920
assert revision.startswith('date:')
922
# Spec for date revisions:
924
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
925
# it can also start with a '+/-/='. '+' says match the first
926
# entry after the given date. '-' is match the first entry before the date
927
# '=' is match the first entry after, but still on the given date.
929
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
930
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
931
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
932
# May 13th, 2005 at 0:00
934
# So the proper way of saying 'give me all entries for today' is:
935
# -r {date:+today}:{date:-tomorrow}
936
# The default is '=' when not supplied
939
if val[:1] in ('+', '-', '='):
940
match_style = val[:1]
943
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
944
if val.lower() == 'yesterday':
945
dt = today - datetime.timedelta(days=1)
946
elif val.lower() == 'today':
948
elif val.lower() == 'tomorrow':
949
dt = today + datetime.timedelta(days=1)
952
# This should be done outside the function to avoid recompiling it.
953
_date_re = re.compile(
954
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
956
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
958
m = _date_re.match(val)
959
if not m or (not m.group('date') and not m.group('time')):
960
raise BzrError('Invalid revision date %r' % revision)
963
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
965
year, month, day = today.year, today.month, today.day
967
hour = int(m.group('hour'))
968
minute = int(m.group('minute'))
969
if m.group('second'):
970
second = int(m.group('second'))
974
hour, minute, second = 0,0,0
976
dt = datetime.datetime(year=year, month=month, day=day,
977
hour=hour, minute=minute, second=second)
981
if match_style == '-':
983
elif match_style == '=':
984
last = dt + datetime.timedelta(days=1)
987
for i in range(len(revs)-1, -1, -1):
988
r = self.get_revision(revs[i])
989
# TODO: Handle timezone.
990
dt = datetime.datetime.fromtimestamp(r.timestamp)
991
if first >= dt and (last is None or dt >= last):
994
for i in range(len(revs)):
995
r = self.get_revision(revs[i])
996
# TODO: Handle timezone.
997
dt = datetime.datetime.fromtimestamp(r.timestamp)
998
if first <= dt and (last is None or dt <= last):
1000
REVISION_NAMESPACES['date:'] = _namespace_date
1002
def revision_tree(self, revision_id):
1003
"""Return Tree for a revision on this branch.
1005
`revision_id` may be None for the null revision, in which case
1006
an `EmptyTree` is returned."""
1007
from bzrlib.tree import EmptyTree, RevisionTree
1008
# TODO: refactor this to use an existing revision object
1009
# so we don't need to read it in twice.
1010
if revision_id == None:
1013
inv = self.get_revision_inventory(revision_id)
1014
return RevisionTree(self.text_store, inv)
1017
def working_tree(self):
1018
"""Return a `Tree` for the working copy."""
1019
from workingtree import WorkingTree
1020
return WorkingTree(self.base, self.read_working_inventory())
1023
def basis_tree(self):
1024
"""Return `Tree` object for last revision.
1026
If there are no revisions yet, return an `EmptyTree`.
1028
from bzrlib.tree import EmptyTree, RevisionTree
1029
r = self.last_patch()
1033
return RevisionTree(self.text_store, self.get_revision_inventory(r))
1037
def rename_one(self, from_rel, to_rel):
1040
This can change the directory or the filename or both.
1044
tree = self.working_tree()
1045
inv = tree.inventory
1046
if not tree.has_filename(from_rel):
1047
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1048
if tree.has_filename(to_rel):
1049
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1051
file_id = inv.path2id(from_rel)
1053
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1055
if inv.path2id(to_rel):
1056
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1058
to_dir, to_tail = os.path.split(to_rel)
1059
to_dir_id = inv.path2id(to_dir)
1060
if to_dir_id == None and to_dir != '':
1061
raise BzrError("can't determine destination directory id for %r" % to_dir)
1063
mutter("rename_one:")
1064
mutter(" file_id {%s}" % file_id)
1065
mutter(" from_rel %r" % from_rel)
1066
mutter(" to_rel %r" % to_rel)
1067
mutter(" to_dir %r" % to_dir)
1068
mutter(" to_dir_id {%s}" % to_dir_id)
1070
inv.rename(file_id, to_dir_id, to_tail)
1072
print "%s => %s" % (from_rel, to_rel)
1074
from_abs = self.abspath(from_rel)
1075
to_abs = self.abspath(to_rel)
1077
os.rename(from_abs, to_abs)
1079
raise BzrError("failed to rename %r to %r: %s"
1080
% (from_abs, to_abs, e[1]),
1081
["rename rolled back"])
1083
self._write_inventory(inv)
1088
def move(self, from_paths, to_name):
1091
to_name must exist as a versioned directory.
1093
If to_name exists and is a directory, the files are moved into
1094
it, keeping their old names. If it is a directory,
1096
Note that to_name is only the last component of the new name;
1097
this doesn't change the directory.
1101
## TODO: Option to move IDs only
1102
assert not isinstance(from_paths, basestring)
1103
tree = self.working_tree()
1104
inv = tree.inventory
1105
to_abs = self.abspath(to_name)
1106
if not isdir(to_abs):
1107
raise BzrError("destination %r is not a directory" % to_abs)
1108
if not tree.has_filename(to_name):
1109
raise BzrError("destination %r not in working directory" % to_abs)
1110
to_dir_id = inv.path2id(to_name)
1111
if to_dir_id == None and to_name != '':
1112
raise BzrError("destination %r is not a versioned directory" % to_name)
1113
to_dir_ie = inv[to_dir_id]
1114
if to_dir_ie.kind not in ('directory', 'root_directory'):
1115
raise BzrError("destination %r is not a directory" % to_abs)
1117
to_idpath = inv.get_idpath(to_dir_id)
1119
for f in from_paths:
1120
if not tree.has_filename(f):
1121
raise BzrError("%r does not exist in working tree" % f)
1122
f_id = inv.path2id(f)
1124
raise BzrError("%r is not versioned" % f)
1125
name_tail = splitpath(f)[-1]
1126
dest_path = appendpath(to_name, name_tail)
1127
if tree.has_filename(dest_path):
1128
raise BzrError("destination %r already exists" % dest_path)
1129
if f_id in to_idpath:
1130
raise BzrError("can't move %r to a subdirectory of itself" % f)
1132
# OK, so there's a race here, it's possible that someone will
1133
# create a file in this interval and then the rename might be
1134
# left half-done. But we should have caught most problems.
1136
for f in from_paths:
1137
name_tail = splitpath(f)[-1]
1138
dest_path = appendpath(to_name, name_tail)
1139
print "%s => %s" % (f, dest_path)
1140
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1142
os.rename(self.abspath(f), self.abspath(dest_path))
1144
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1145
["rename rolled back"])
1147
self._write_inventory(inv)
1152
def revert(self, filenames, old_tree=None, backups=True):
1153
"""Restore selected files to the versions from a previous tree.
1156
If true (default) backups are made of files before
1159
from bzrlib.errors import NotVersionedError, BzrError
1160
from bzrlib.atomicfile import AtomicFile
1161
from bzrlib.osutils import backup_file
1163
inv = self.read_working_inventory()
1164
if old_tree is None:
1165
old_tree = self.basis_tree()
1166
old_inv = old_tree.inventory
1169
for fn in filenames:
1170
file_id = inv.path2id(fn)
1172
raise NotVersionedError("not a versioned file", fn)
1173
if not old_inv.has_id(file_id):
1174
raise BzrError("file not present in old tree", fn, file_id)
1175
nids.append((fn, file_id))
1177
# TODO: Rename back if it was previously at a different location
1179
# TODO: If given a directory, restore the entire contents from
1180
# the previous version.
1182
# TODO: Make a backup to a temporary file.
1184
# TODO: If the file previously didn't exist, delete it?
1185
for fn, file_id in nids:
1188
f = AtomicFile(fn, 'wb')
1190
f.write(old_tree.get_file(file_id).read())
1196
def pending_merges(self):
1197
"""Return a list of pending merges.
1199
These are revisions that have been merged into the working
1200
directory but not yet committed.
1202
cfn = self.controlfilename('pending-merges')
1203
if not os.path.exists(cfn):
1206
for l in self.controlfile('pending-merges', 'r').readlines():
1207
p.append(l.rstrip('\n'))
1211
def add_pending_merge(self, revision_id):
1212
from bzrlib.revision import validate_revision_id
1214
validate_revision_id(revision_id)
1216
p = self.pending_merges()
1217
if revision_id in p:
1219
p.append(revision_id)
1220
self.set_pending_merges(p)
1223
def set_pending_merges(self, rev_list):
1224
from bzrlib.atomicfile import AtomicFile
1227
f = AtomicFile(self.controlfilename('pending-merges'))
1239
class ScratchBranch(Branch):
1240
"""Special test class: a branch that cleans up after itself.
1242
>>> b = ScratchBranch()
1250
def __init__(self, files=[], dirs=[], base=None):
1251
"""Make a test branch.
1253
This creates a temporary directory and runs init-tree in it.
1255
If any files are listed, they are created in the working copy.
1257
from tempfile import mkdtemp
1262
Branch.__init__(self, base, init=init)
1264
os.mkdir(self.abspath(d))
1267
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1272
>>> orig = ScratchBranch(files=["file1", "file2"])
1273
>>> clone = orig.clone()
1274
>>> os.path.samefile(orig.base, clone.base)
1276
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1279
from shutil import copytree
1280
from tempfile import mkdtemp
1283
copytree(self.base, base, symlinks=True)
1284
return ScratchBranch(base=base)
1290
"""Destroy the test branch, removing the scratch directory."""
1291
from shutil import rmtree
1294
mutter("delete ScratchBranch %s" % self.base)
1297
# Work around for shutil.rmtree failing on Windows when
1298
# readonly files are encountered
1299
mutter("hit exception in destroying ScratchBranch: %s" % e)
1300
for root, dirs, files in os.walk(self.base, topdown=False):
1302
os.chmod(os.path.join(root, name), 0700)
1308
######################################################################
1312
def is_control_file(filename):
1313
## FIXME: better check
1314
filename = os.path.normpath(filename)
1315
while filename != '':
1316
head, tail = os.path.split(filename)
1317
## mutter('check %r for control file' % ((head, tail), ))
1318
if tail == bzrlib.BZRDIR:
1320
if filename == head:
1327
def gen_file_id(name):
1328
"""Return new file id.
1330
This should probably generate proper UUIDs, but for the moment we
1331
cope with just randomness because running uuidgen every time is
1334
from binascii import hexlify
1335
from time import time
1337
# get last component
1338
idx = name.rfind('/')
1340
name = name[idx+1 : ]
1341
idx = name.rfind('\\')
1343
name = name[idx+1 : ]
1345
# make it not a hidden file
1346
name = name.lstrip('.')
1348
# remove any wierd characters; we don't escape them but rather
1349
# just pull them out
1350
name = re.sub(r'[^\w.]', '', name)
1352
s = hexlify(rand_bytes(8))
1353
return '-'.join((name, compact_date(time()), s))