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_ids):
553
from bzrlib.atomicfile import AtomicFile
555
for revision_id in revision_ids:
556
mutter("add {%s} to revision-history" % revision_id)
558
rev_history = self.revision_history()
559
rev_history.extend(revision_ids)
561
f = AtomicFile(self.controlfilename('revision-history'))
563
for rev_id in rev_history:
570
def get_revision(self, revision_id):
571
"""Return the Revision object for a named revision"""
572
from bzrlib.revision import Revision
573
from bzrlib.xml import unpack_xml
577
if not revision_id or not isinstance(revision_id, basestring):
578
raise ValueError('invalid revision-id: %r' % revision_id)
579
r = unpack_xml(Revision, self.revision_store[revision_id])
583
assert r.revision_id == revision_id
587
def get_revision_sha1(self, revision_id):
588
"""Hash the stored value of a revision, and return it."""
589
# In the future, revision entries will be signed. At that
590
# point, it is probably best *not* to include the signature
591
# in the revision hash. Because that lets you re-sign
592
# the revision, (add signatures/remove signatures) and still
593
# have all hash pointers stay consistent.
594
# But for now, just hash the contents.
595
return sha_file(self.revision_store[revision_id])
598
def get_inventory(self, inventory_id):
599
"""Get Inventory object by hash.
601
TODO: Perhaps for this and similar methods, take a revision
602
parameter which can be either an integer revno or a
604
from bzrlib.inventory import Inventory
605
from bzrlib.xml import unpack_xml
607
return unpack_xml(Inventory, self.inventory_store[inventory_id])
610
def get_inventory_sha1(self, inventory_id):
611
"""Return the sha1 hash of the inventory entry
613
return sha_file(self.inventory_store[inventory_id])
616
def get_revision_inventory(self, revision_id):
617
"""Return inventory of a past revision."""
618
# bzr 0.0.6 imposes the constraint that the inventory_id
619
# must be the same as its revision, so this is trivial.
620
if revision_id == None:
621
from bzrlib.inventory import Inventory
624
return self.get_inventory(revision_id)
627
def revision_history(self):
628
"""Return sequence of revision hashes on to this branch.
630
>>> ScratchBranch().revision_history()
635
return [l.rstrip('\r\n') for l in
636
self.controlfile('revision-history', 'r').readlines()]
641
def common_ancestor(self, other, self_revno=None, other_revno=None):
644
>>> sb = ScratchBranch(files=['foo', 'foo~'])
645
>>> sb.common_ancestor(sb) == (None, None)
647
>>> commit.commit(sb, "Committing first revision", verbose=False)
648
>>> sb.common_ancestor(sb)[0]
650
>>> clone = sb.clone()
651
>>> commit.commit(sb, "Committing second revision", verbose=False)
652
>>> sb.common_ancestor(sb)[0]
654
>>> sb.common_ancestor(clone)[0]
656
>>> commit.commit(clone, "Committing divergent second revision",
658
>>> sb.common_ancestor(clone)[0]
660
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
662
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
664
>>> clone2 = sb.clone()
665
>>> sb.common_ancestor(clone2)[0]
667
>>> sb.common_ancestor(clone2, self_revno=1)[0]
669
>>> sb.common_ancestor(clone2, other_revno=1)[0]
672
my_history = self.revision_history()
673
other_history = other.revision_history()
674
if self_revno is None:
675
self_revno = len(my_history)
676
if other_revno is None:
677
other_revno = len(other_history)
678
indices = range(min((self_revno, other_revno)))
681
if my_history[r] == other_history[r]:
682
return r+1, my_history[r]
685
def enum_history(self, direction):
686
"""Return (revno, revision_id) for history of branch.
689
'forward' is from earliest to latest
690
'reverse' is from latest to earliest
692
rh = self.revision_history()
693
if direction == 'forward':
698
elif direction == 'reverse':
704
raise ValueError('invalid history direction', direction)
708
"""Return current revision number for this branch.
710
That is equivalent to the number of revisions committed to
713
return len(self.revision_history())
716
def last_patch(self):
717
"""Return last patch hash, or None if no history.
719
ph = self.revision_history()
726
def missing_revisions(self, other, stop_revision=None):
728
If self and other have not diverged, return a list of the revisions
729
present in other, but missing from self.
731
>>> from bzrlib.commit import commit
732
>>> bzrlib.trace.silent = True
733
>>> br1 = ScratchBranch()
734
>>> br2 = ScratchBranch()
735
>>> br1.missing_revisions(br2)
737
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
738
>>> br1.missing_revisions(br2)
740
>>> br2.missing_revisions(br1)
742
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
743
>>> br1.missing_revisions(br2)
745
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
746
>>> br1.missing_revisions(br2)
748
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
749
>>> br1.missing_revisions(br2)
750
Traceback (most recent call last):
751
DivergedBranches: These branches have diverged.
753
self_history = self.revision_history()
754
self_len = len(self_history)
755
other_history = other.revision_history()
756
other_len = len(other_history)
757
common_index = min(self_len, other_len) -1
758
if common_index >= 0 and \
759
self_history[common_index] != other_history[common_index]:
760
raise DivergedBranches(self, other)
762
if stop_revision is None:
763
stop_revision = other_len
764
elif stop_revision > other_len:
765
raise NoSuchRevision(self, stop_revision)
767
return other_history[self_len:stop_revision]
770
def update_revisions(self, other, stop_revision=None):
771
"""Pull in all new revisions from other branch.
773
>>> from bzrlib.commit import commit
774
>>> bzrlib.trace.silent = True
775
>>> br1 = ScratchBranch(files=['foo', 'bar'])
778
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
779
>>> br2 = ScratchBranch()
780
>>> br2.update_revisions(br1)
784
>>> br2.revision_history()
786
>>> br2.update_revisions(br1)
790
>>> br1.text_store.total_size() == br2.text_store.total_size()
793
from bzrlib.progress import ProgressBar
797
from sets import Set as set
801
pb.update('comparing histories')
802
revision_ids = self.missing_revisions(other, stop_revision)
804
if hasattr(other.revision_store, "prefetch"):
805
other.revision_store.prefetch(revision_ids)
806
if hasattr(other.inventory_store, "prefetch"):
807
inventory_ids = [other.get_revision(r).inventory_id
808
for r in revision_ids]
809
other.inventory_store.prefetch(inventory_ids)
814
for rev_id in revision_ids:
816
pb.update('fetching revision', i, len(revision_ids))
817
rev = other.get_revision(rev_id)
818
revisions.append(rev)
819
inv = other.get_inventory(str(rev.inventory_id))
820
for key, entry in inv.iter_entries():
821
if entry.text_id is None:
823
if entry.text_id not in self.text_store:
824
needed_texts.add(entry.text_id)
828
count = self.text_store.copy_multi(other.text_store, needed_texts)
829
print "Added %d texts." % count
830
inventory_ids = [ f.inventory_id for f in revisions ]
831
count = self.inventory_store.copy_multi(other.inventory_store,
833
print "Added %d inventories." % count
834
revision_ids = [ f.revision_id for f in revisions]
835
count = self.revision_store.copy_multi(other.revision_store,
837
for revision_id in revision_ids:
838
self.append_revision(revision_id)
839
print "Added %d revisions." % count
842
def commit(self, *args, **kw):
843
from bzrlib.commit import commit
844
commit(self, *args, **kw)
847
def lookup_revision(self, revision):
848
"""Return the revision identifier for a given revision information."""
849
revno, info = self.get_revision_info(revision)
852
def get_revision_info(self, revision):
853
"""Return (revno, revision id) for revision identifier.
855
revision can be an integer, in which case it is assumed to be revno (though
856
this will translate negative values into positive ones)
857
revision can also be a string, in which case it is parsed for something like
858
'date:' or 'revid:' etc.
863
try:# Convert to int if possible
864
revision = int(revision)
867
revs = self.revision_history()
868
if isinstance(revision, int):
871
# Mabye we should do this first, but we don't need it if revision == 0
873
revno = len(revs) + revision + 1
876
elif isinstance(revision, basestring):
877
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
878
if revision.startswith(prefix):
879
revno = func(self, revs, revision)
882
raise BzrError('No namespace registered for string: %r' % revision)
884
if revno is None or revno <= 0 or revno > len(revs):
885
raise BzrError("no such revision %s" % revision)
886
return revno, revs[revno-1]
888
def _namespace_revno(self, revs, revision):
889
"""Lookup a revision by revision number"""
890
assert revision.startswith('revno:')
892
return int(revision[6:])
895
REVISION_NAMESPACES['revno:'] = _namespace_revno
897
def _namespace_revid(self, revs, revision):
898
assert revision.startswith('revid:')
900
return revs.index(revision[6:]) + 1
903
REVISION_NAMESPACES['revid:'] = _namespace_revid
905
def _namespace_last(self, revs, revision):
906
assert revision.startswith('last:')
908
offset = int(revision[5:])
913
raise BzrError('You must supply a positive value for --revision last:XXX')
914
return len(revs) - offset + 1
915
REVISION_NAMESPACES['last:'] = _namespace_last
917
def _namespace_tag(self, revs, revision):
918
assert revision.startswith('tag:')
919
raise BzrError('tag: namespace registered, but not implemented.')
920
REVISION_NAMESPACES['tag:'] = _namespace_tag
922
def _namespace_date(self, revs, revision):
923
assert revision.startswith('date:')
925
# Spec for date revisions:
927
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
928
# it can also start with a '+/-/='. '+' says match the first
929
# entry after the given date. '-' is match the first entry before the date
930
# '=' is match the first entry after, but still on the given date.
932
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
933
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
934
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
935
# May 13th, 2005 at 0:00
937
# So the proper way of saying 'give me all entries for today' is:
938
# -r {date:+today}:{date:-tomorrow}
939
# The default is '=' when not supplied
942
if val[:1] in ('+', '-', '='):
943
match_style = val[:1]
946
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
947
if val.lower() == 'yesterday':
948
dt = today - datetime.timedelta(days=1)
949
elif val.lower() == 'today':
951
elif val.lower() == 'tomorrow':
952
dt = today + datetime.timedelta(days=1)
955
# This should be done outside the function to avoid recompiling it.
956
_date_re = re.compile(
957
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
959
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
961
m = _date_re.match(val)
962
if not m or (not m.group('date') and not m.group('time')):
963
raise BzrError('Invalid revision date %r' % revision)
966
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
968
year, month, day = today.year, today.month, today.day
970
hour = int(m.group('hour'))
971
minute = int(m.group('minute'))
972
if m.group('second'):
973
second = int(m.group('second'))
977
hour, minute, second = 0,0,0
979
dt = datetime.datetime(year=year, month=month, day=day,
980
hour=hour, minute=minute, second=second)
984
if match_style == '-':
986
elif match_style == '=':
987
last = dt + datetime.timedelta(days=1)
990
for i in range(len(revs)-1, -1, -1):
991
r = self.get_revision(revs[i])
992
# TODO: Handle timezone.
993
dt = datetime.datetime.fromtimestamp(r.timestamp)
994
if first >= dt and (last is None or dt >= last):
997
for i in range(len(revs)):
998
r = self.get_revision(revs[i])
999
# TODO: Handle timezone.
1000
dt = datetime.datetime.fromtimestamp(r.timestamp)
1001
if first <= dt and (last is None or dt <= last):
1003
REVISION_NAMESPACES['date:'] = _namespace_date
1005
def revision_tree(self, revision_id):
1006
"""Return Tree for a revision on this branch.
1008
`revision_id` may be None for the null revision, in which case
1009
an `EmptyTree` is returned."""
1010
from bzrlib.tree import EmptyTree, RevisionTree
1011
# TODO: refactor this to use an existing revision object
1012
# so we don't need to read it in twice.
1013
if revision_id == None:
1016
inv = self.get_revision_inventory(revision_id)
1017
return RevisionTree(self.text_store, inv)
1020
def working_tree(self):
1021
"""Return a `Tree` for the working copy."""
1022
from workingtree import WorkingTree
1023
return WorkingTree(self.base, self.read_working_inventory())
1026
def basis_tree(self):
1027
"""Return `Tree` object for last revision.
1029
If there are no revisions yet, return an `EmptyTree`.
1031
from bzrlib.tree import EmptyTree, RevisionTree
1032
r = self.last_patch()
1036
return RevisionTree(self.text_store, self.get_revision_inventory(r))
1040
def rename_one(self, from_rel, to_rel):
1043
This can change the directory or the filename or both.
1047
tree = self.working_tree()
1048
inv = tree.inventory
1049
if not tree.has_filename(from_rel):
1050
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1051
if tree.has_filename(to_rel):
1052
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1054
file_id = inv.path2id(from_rel)
1056
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1058
if inv.path2id(to_rel):
1059
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1061
to_dir, to_tail = os.path.split(to_rel)
1062
to_dir_id = inv.path2id(to_dir)
1063
if to_dir_id == None and to_dir != '':
1064
raise BzrError("can't determine destination directory id for %r" % to_dir)
1066
mutter("rename_one:")
1067
mutter(" file_id {%s}" % file_id)
1068
mutter(" from_rel %r" % from_rel)
1069
mutter(" to_rel %r" % to_rel)
1070
mutter(" to_dir %r" % to_dir)
1071
mutter(" to_dir_id {%s}" % to_dir_id)
1073
inv.rename(file_id, to_dir_id, to_tail)
1075
print "%s => %s" % (from_rel, to_rel)
1077
from_abs = self.abspath(from_rel)
1078
to_abs = self.abspath(to_rel)
1080
os.rename(from_abs, to_abs)
1082
raise BzrError("failed to rename %r to %r: %s"
1083
% (from_abs, to_abs, e[1]),
1084
["rename rolled back"])
1086
self._write_inventory(inv)
1091
def move(self, from_paths, to_name):
1094
to_name must exist as a versioned directory.
1096
If to_name exists and is a directory, the files are moved into
1097
it, keeping their old names. If it is a directory,
1099
Note that to_name is only the last component of the new name;
1100
this doesn't change the directory.
1104
## TODO: Option to move IDs only
1105
assert not isinstance(from_paths, basestring)
1106
tree = self.working_tree()
1107
inv = tree.inventory
1108
to_abs = self.abspath(to_name)
1109
if not isdir(to_abs):
1110
raise BzrError("destination %r is not a directory" % to_abs)
1111
if not tree.has_filename(to_name):
1112
raise BzrError("destination %r not in working directory" % to_abs)
1113
to_dir_id = inv.path2id(to_name)
1114
if to_dir_id == None and to_name != '':
1115
raise BzrError("destination %r is not a versioned directory" % to_name)
1116
to_dir_ie = inv[to_dir_id]
1117
if to_dir_ie.kind not in ('directory', 'root_directory'):
1118
raise BzrError("destination %r is not a directory" % to_abs)
1120
to_idpath = inv.get_idpath(to_dir_id)
1122
for f in from_paths:
1123
if not tree.has_filename(f):
1124
raise BzrError("%r does not exist in working tree" % f)
1125
f_id = inv.path2id(f)
1127
raise BzrError("%r is not versioned" % f)
1128
name_tail = splitpath(f)[-1]
1129
dest_path = appendpath(to_name, name_tail)
1130
if tree.has_filename(dest_path):
1131
raise BzrError("destination %r already exists" % dest_path)
1132
if f_id in to_idpath:
1133
raise BzrError("can't move %r to a subdirectory of itself" % f)
1135
# OK, so there's a race here, it's possible that someone will
1136
# create a file in this interval and then the rename might be
1137
# left half-done. But we should have caught most problems.
1139
for f in from_paths:
1140
name_tail = splitpath(f)[-1]
1141
dest_path = appendpath(to_name, name_tail)
1142
print "%s => %s" % (f, dest_path)
1143
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1145
os.rename(self.abspath(f), self.abspath(dest_path))
1147
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1148
["rename rolled back"])
1150
self._write_inventory(inv)
1155
def revert(self, filenames, old_tree=None, backups=True):
1156
"""Restore selected files to the versions from a previous tree.
1159
If true (default) backups are made of files before
1162
from bzrlib.errors import NotVersionedError, BzrError
1163
from bzrlib.atomicfile import AtomicFile
1164
from bzrlib.osutils import backup_file
1166
inv = self.read_working_inventory()
1167
if old_tree is None:
1168
old_tree = self.basis_tree()
1169
old_inv = old_tree.inventory
1172
for fn in filenames:
1173
file_id = inv.path2id(fn)
1175
raise NotVersionedError("not a versioned file", fn)
1176
if not old_inv.has_id(file_id):
1177
raise BzrError("file not present in old tree", fn, file_id)
1178
nids.append((fn, file_id))
1180
# TODO: Rename back if it was previously at a different location
1182
# TODO: If given a directory, restore the entire contents from
1183
# the previous version.
1185
# TODO: Make a backup to a temporary file.
1187
# TODO: If the file previously didn't exist, delete it?
1188
for fn, file_id in nids:
1191
f = AtomicFile(fn, 'wb')
1193
f.write(old_tree.get_file(file_id).read())
1199
def pending_merges(self):
1200
"""Return a list of pending merges.
1202
These are revisions that have been merged into the working
1203
directory but not yet committed.
1205
cfn = self.controlfilename('pending-merges')
1206
if not os.path.exists(cfn):
1209
for l in self.controlfile('pending-merges', 'r').readlines():
1210
p.append(l.rstrip('\n'))
1214
def add_pending_merge(self, revision_id):
1215
from bzrlib.revision import validate_revision_id
1217
validate_revision_id(revision_id)
1219
p = self.pending_merges()
1220
if revision_id in p:
1222
p.append(revision_id)
1223
self.set_pending_merges(p)
1226
def set_pending_merges(self, rev_list):
1227
from bzrlib.atomicfile import AtomicFile
1230
f = AtomicFile(self.controlfilename('pending-merges'))
1242
class ScratchBranch(Branch):
1243
"""Special test class: a branch that cleans up after itself.
1245
>>> b = ScratchBranch()
1253
def __init__(self, files=[], dirs=[], base=None):
1254
"""Make a test branch.
1256
This creates a temporary directory and runs init-tree in it.
1258
If any files are listed, they are created in the working copy.
1260
from tempfile import mkdtemp
1265
Branch.__init__(self, base, init=init)
1267
os.mkdir(self.abspath(d))
1270
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1275
>>> orig = ScratchBranch(files=["file1", "file2"])
1276
>>> clone = orig.clone()
1277
>>> os.path.samefile(orig.base, clone.base)
1279
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1282
from shutil import copytree
1283
from tempfile import mkdtemp
1286
copytree(self.base, base, symlinks=True)
1287
return ScratchBranch(base=base)
1293
"""Destroy the test branch, removing the scratch directory."""
1294
from shutil import rmtree
1297
mutter("delete ScratchBranch %s" % self.base)
1300
# Work around for shutil.rmtree failing on Windows when
1301
# readonly files are encountered
1302
mutter("hit exception in destroying ScratchBranch: %s" % e)
1303
for root, dirs, files in os.walk(self.base, topdown=False):
1305
os.chmod(os.path.join(root, name), 0700)
1311
######################################################################
1315
def is_control_file(filename):
1316
## FIXME: better check
1317
filename = os.path.normpath(filename)
1318
while filename != '':
1319
head, tail = os.path.split(filename)
1320
## mutter('check %r for control file' % ((head, tail), ))
1321
if tail == bzrlib.BZRDIR:
1323
if filename == head:
1330
def gen_file_id(name):
1331
"""Return new file id.
1333
This should probably generate proper UUIDs, but for the moment we
1334
cope with just randomness because running uuidgen every time is
1337
from binascii import hexlify
1338
from time import time
1340
# get last component
1341
idx = name.rfind('/')
1343
name = name[idx+1 : ]
1344
idx = name.rfind('\\')
1346
name = name[idx+1 : ]
1348
# make it not a hidden file
1349
name = name.lstrip('.')
1351
# remove any wierd characters; we don't escape them but rather
1352
# just pull them out
1353
name = re.sub(r'[^\w.]', '', name)
1355
s = hexlify(rand_bytes(8))
1356
return '-'.join((name, compact_date(time()), s))