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)
951
# This should be done outside the function to avoid recompiling it.
952
_date_re = re.compile(
953
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
955
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
957
m = _date_re.match(val)
958
if not m or (not m.group('date') and not m.group('time')):
959
raise BzrError('Invalid revision date %r' % revision)
962
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
964
year, month, day = today.year, today.month, today.day
966
hour = int(m.group('hour'))
967
minute = int(m.group('minute'))
968
if m.group('second'):
969
second = int(m.group('second'))
973
hour, minute, second = 0,0,0
975
dt = datetime.datetime(year=year, month=month, day=day,
976
hour=hour, minute=minute, second=second)
980
if match_style == '-':
982
elif match_style == '=':
983
last = dt + datetime.timedelta(days=1)
986
for i in range(len(revs)-1, -1, -1):
987
r = self.get_revision(revs[i])
988
# TODO: Handle timezone.
989
dt = datetime.datetime.fromtimestamp(r.timestamp)
990
if first >= dt and (last is None or dt >= last):
993
for i in range(len(revs)):
994
r = self.get_revision(revs[i])
995
# TODO: Handle timezone.
996
dt = datetime.datetime.fromtimestamp(r.timestamp)
997
if first <= dt and (last is None or dt <= last):
999
REVISION_NAMESPACES['date:'] = _namespace_date
1001
def revision_tree(self, revision_id):
1002
"""Return Tree for a revision on this branch.
1004
`revision_id` may be None for the null revision, in which case
1005
an `EmptyTree` is returned."""
1006
from bzrlib.tree import EmptyTree, RevisionTree
1007
# TODO: refactor this to use an existing revision object
1008
# so we don't need to read it in twice.
1009
if revision_id == None:
1012
inv = self.get_revision_inventory(revision_id)
1013
return RevisionTree(self.text_store, inv)
1016
def working_tree(self):
1017
"""Return a `Tree` for the working copy."""
1018
from workingtree import WorkingTree
1019
return WorkingTree(self.base, self.read_working_inventory())
1022
def basis_tree(self):
1023
"""Return `Tree` object for last revision.
1025
If there are no revisions yet, return an `EmptyTree`.
1027
from bzrlib.tree import EmptyTree, RevisionTree
1028
r = self.last_patch()
1032
return RevisionTree(self.text_store, self.get_revision_inventory(r))
1036
def rename_one(self, from_rel, to_rel):
1039
This can change the directory or the filename or both.
1043
tree = self.working_tree()
1044
inv = tree.inventory
1045
if not tree.has_filename(from_rel):
1046
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1047
if tree.has_filename(to_rel):
1048
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1050
file_id = inv.path2id(from_rel)
1052
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1054
if inv.path2id(to_rel):
1055
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1057
to_dir, to_tail = os.path.split(to_rel)
1058
to_dir_id = inv.path2id(to_dir)
1059
if to_dir_id == None and to_dir != '':
1060
raise BzrError("can't determine destination directory id for %r" % to_dir)
1062
mutter("rename_one:")
1063
mutter(" file_id {%s}" % file_id)
1064
mutter(" from_rel %r" % from_rel)
1065
mutter(" to_rel %r" % to_rel)
1066
mutter(" to_dir %r" % to_dir)
1067
mutter(" to_dir_id {%s}" % to_dir_id)
1069
inv.rename(file_id, to_dir_id, to_tail)
1071
print "%s => %s" % (from_rel, to_rel)
1073
from_abs = self.abspath(from_rel)
1074
to_abs = self.abspath(to_rel)
1076
os.rename(from_abs, to_abs)
1078
raise BzrError("failed to rename %r to %r: %s"
1079
% (from_abs, to_abs, e[1]),
1080
["rename rolled back"])
1082
self._write_inventory(inv)
1087
def move(self, from_paths, to_name):
1090
to_name must exist as a versioned directory.
1092
If to_name exists and is a directory, the files are moved into
1093
it, keeping their old names. If it is a directory,
1095
Note that to_name is only the last component of the new name;
1096
this doesn't change the directory.
1100
## TODO: Option to move IDs only
1101
assert not isinstance(from_paths, basestring)
1102
tree = self.working_tree()
1103
inv = tree.inventory
1104
to_abs = self.abspath(to_name)
1105
if not isdir(to_abs):
1106
raise BzrError("destination %r is not a directory" % to_abs)
1107
if not tree.has_filename(to_name):
1108
raise BzrError("destination %r not in working directory" % to_abs)
1109
to_dir_id = inv.path2id(to_name)
1110
if to_dir_id == None and to_name != '':
1111
raise BzrError("destination %r is not a versioned directory" % to_name)
1112
to_dir_ie = inv[to_dir_id]
1113
if to_dir_ie.kind not in ('directory', 'root_directory'):
1114
raise BzrError("destination %r is not a directory" % to_abs)
1116
to_idpath = inv.get_idpath(to_dir_id)
1118
for f in from_paths:
1119
if not tree.has_filename(f):
1120
raise BzrError("%r does not exist in working tree" % f)
1121
f_id = inv.path2id(f)
1123
raise BzrError("%r is not versioned" % f)
1124
name_tail = splitpath(f)[-1]
1125
dest_path = appendpath(to_name, name_tail)
1126
if tree.has_filename(dest_path):
1127
raise BzrError("destination %r already exists" % dest_path)
1128
if f_id in to_idpath:
1129
raise BzrError("can't move %r to a subdirectory of itself" % f)
1131
# OK, so there's a race here, it's possible that someone will
1132
# create a file in this interval and then the rename might be
1133
# left half-done. But we should have caught most problems.
1135
for f in from_paths:
1136
name_tail = splitpath(f)[-1]
1137
dest_path = appendpath(to_name, name_tail)
1138
print "%s => %s" % (f, dest_path)
1139
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1141
os.rename(self.abspath(f), self.abspath(dest_path))
1143
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1144
["rename rolled back"])
1146
self._write_inventory(inv)
1151
def revert(self, filenames, old_tree=None, backups=True):
1152
"""Restore selected files to the versions from a previous tree.
1155
If true (default) backups are made of files before
1158
from bzrlib.errors import NotVersionedError, BzrError
1159
from bzrlib.atomicfile import AtomicFile
1160
from bzrlib.osutils import backup_file
1162
inv = self.read_working_inventory()
1163
if old_tree is None:
1164
old_tree = self.basis_tree()
1165
old_inv = old_tree.inventory
1168
for fn in filenames:
1169
file_id = inv.path2id(fn)
1171
raise NotVersionedError("not a versioned file", fn)
1172
if not old_inv.has_id(file_id):
1173
raise BzrError("file not present in old tree", fn, file_id)
1174
nids.append((fn, file_id))
1176
# TODO: Rename back if it was previously at a different location
1178
# TODO: If given a directory, restore the entire contents from
1179
# the previous version.
1181
# TODO: Make a backup to a temporary file.
1183
# TODO: If the file previously didn't exist, delete it?
1184
for fn, file_id in nids:
1187
f = AtomicFile(fn, 'wb')
1189
f.write(old_tree.get_file(file_id).read())
1195
def pending_merges(self):
1196
"""Return a list of pending merges.
1198
These are revisions that have been merged into the working
1199
directory but not yet committed.
1201
cfn = self.controlfilename('pending-merges')
1202
if not os.path.exists(cfn):
1205
for l in self.controlfile('pending-merges', 'r').readlines():
1206
p.append(l.rstrip('\n'))
1210
def add_pending_merge(self, revision_id):
1211
from bzrlib.revision import validate_revision_id
1213
validate_revision_id(revision_id)
1215
p = self.pending_merges()
1216
if revision_id in p:
1218
p.append(revision_id)
1219
self.set_pending_merges(p)
1222
def set_pending_merges(self, rev_list):
1223
from bzrlib.atomicfile import AtomicFile
1226
f = AtomicFile(self.controlfilename('pending-merges'))
1238
class ScratchBranch(Branch):
1239
"""Special test class: a branch that cleans up after itself.
1241
>>> b = ScratchBranch()
1249
def __init__(self, files=[], dirs=[], base=None):
1250
"""Make a test branch.
1252
This creates a temporary directory and runs init-tree in it.
1254
If any files are listed, they are created in the working copy.
1256
from tempfile import mkdtemp
1261
Branch.__init__(self, base, init=init)
1263
os.mkdir(self.abspath(d))
1266
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1271
>>> orig = ScratchBranch(files=["file1", "file2"])
1272
>>> clone = orig.clone()
1273
>>> os.path.samefile(orig.base, clone.base)
1275
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1278
from shutil import copytree
1279
from tempfile import mkdtemp
1282
copytree(self.base, base, symlinks=True)
1283
return ScratchBranch(base=base)
1289
"""Destroy the test branch, removing the scratch directory."""
1290
from shutil import rmtree
1293
mutter("delete ScratchBranch %s" % self.base)
1296
# Work around for shutil.rmtree failing on Windows when
1297
# readonly files are encountered
1298
mutter("hit exception in destroying ScratchBranch: %s" % e)
1299
for root, dirs, files in os.walk(self.base, topdown=False):
1301
os.chmod(os.path.join(root, name), 0700)
1307
######################################################################
1311
def is_control_file(filename):
1312
## FIXME: better check
1313
filename = os.path.normpath(filename)
1314
while filename != '':
1315
head, tail = os.path.split(filename)
1316
## mutter('check %r for control file' % ((head, tail), ))
1317
if tail == bzrlib.BZRDIR:
1319
if filename == head:
1326
def gen_file_id(name):
1327
"""Return new file id.
1329
This should probably generate proper UUIDs, but for the moment we
1330
cope with just randomness because running uuidgen every time is
1333
from binascii import hexlify
1334
from time import time
1336
# get last component
1337
idx = name.rfind('/')
1339
name = name[idx+1 : ]
1340
idx = name.rfind('\\')
1342
name = name[idx+1 : ]
1344
# make it not a hidden file
1345
name = name.lstrip('.')
1347
# remove any wierd characters; we don't escape them but rather
1348
# just pull them out
1349
name = re.sub(r'[^\w.]', '', name)
1351
s = hexlify(rand_bytes(8))
1352
return '-'.join((name, compact_date(time()), s))