15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
import traceback, socket, fnmatch, difflib, time
22
from binascii import hexlify
22
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
29
from bzrlib.textui import show_status
30
from bzrlib.revision import Revision
31
from bzrlib.xml import unpack_xml
32
from bzrlib.delta import compare_trees
33
from bzrlib.tree import EmptyTree, RevisionTree
25
from inventory import Inventory
26
from trace import mutter, note
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
from inventory import InventoryEntry, Inventory
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
32
from store import ImmutableStore
33
from revision import Revision
34
from errors import bailout, BzrError
35
from textui import show_status
36
from diff import diff_trees
38
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
39
## TODO: Maybe include checks for common corruption of newlines, etc?
42
# TODO: Some operations like log might retrieve the same revisions
43
# repeatedly to calculate deltas. We could perhaps have a weakref
44
# cache in memory to make this faster.
46
# TODO: please move the revision-string syntax stuff out of the branch
47
# object; it's clutter
50
def find_branch(f, **args):
51
if f and (f.startswith('http://') or f.startswith('https://')):
53
return remotebranch.RemoteBranch(f, **args)
55
return Branch(f, **args)
58
def find_cached_branch(f, cache_root, **args):
59
from remotebranch import RemoteBranch
60
br = find_branch(f, **args)
61
def cacheify(br, store_name):
62
from meta_store import CachedStore
63
cache_path = os.path.join(cache_root, store_name)
65
new_store = CachedStore(getattr(br, store_name), cache_path)
66
setattr(br, store_name, new_store)
68
if isinstance(br, RemoteBranch):
69
cacheify(br, 'inventory_store')
70
cacheify(br, 'text_store')
71
cacheify(br, 'revision_store')
75
def _relpath(base, path):
76
"""Return path relative to base, or raise exception.
78
The path may be either an absolute path or a path relative to the
79
current working directory.
81
Lifted out of Branch.relpath for ease of testing.
83
os.path.commonprefix (python2.4) has a bad bug that it works just
84
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
85
avoids that problem."""
86
rp = os.path.abspath(path)
90
while len(head) >= len(base):
93
head, tail = os.path.split(head)
97
from errors import NotBranchError
98
raise NotBranchError("path %r is not within branch %r" % (rp, base))
100
return os.sep.join(s)
103
43
def find_branch_root(f=None):
104
44
"""Find the branch root enclosing f, or pwd.
106
f may be a filename or a URL.
108
46
It is not necessary that f exists.
110
48
Basically we keep looking up until we find the control directory or
111
run into the root. If there isn't one, raises NotBranchError.
115
52
elif hasattr(os.path, 'realpath'):
116
53
f = os.path.realpath(f)
118
55
f = os.path.abspath(f)
119
if not os.path.exists(f):
120
raise BzrError('%r does not exist' % f)
212
119
__repr__ = __str__
216
if self._lock_mode or self._lock:
217
from warnings import warn
218
warn("branch %r was not explicitly unlocked" % self)
222
def lock_write(self):
224
if self._lock_mode != 'w':
225
from errors import LockError
226
raise LockError("can't upgrade to a write lock from %r" %
228
self._lock_count += 1
230
from bzrlib.lock import WriteLock
232
self._lock = WriteLock(self.controlfilename('branch-lock'))
233
self._lock_mode = 'w'
239
assert self._lock_mode in ('r', 'w'), \
240
"invalid lock mode %r" % self._lock_mode
241
self._lock_count += 1
243
from bzrlib.lock import ReadLock
245
self._lock = ReadLock(self.controlfilename('branch-lock'))
246
self._lock_mode = 'r'
250
if not self._lock_mode:
251
from errors import LockError
252
raise LockError('branch %r is not locked' % (self))
254
if self._lock_count > 1:
255
self._lock_count -= 1
259
self._lock_mode = self._lock_count = None
123
def lock(self, mode='w'):
124
"""Lock the on-disk branch, excluding other processes."""
130
om = os.O_WRONLY | os.O_CREAT
135
raise BzrError("invalid locking mode %r" % mode)
137
# XXX: Old branches might not have the lock file, and
138
# won't get one until someone does a write-mode command on
139
# them or creates it by hand.
141
lockfile = os.open(self.controlfilename('branch-lock'), om)
142
fcntl.lockf(lockfile, lm)
144
fcntl.lockf(lockfile, fcntl.LOCK_UN)
146
self._lockmode = None
148
self._lockmode = mode
150
warning("please write a locking method for platform %r" % sys.platform)
152
self._lockmode = None
154
self._lockmode = mode
157
def _need_readlock(self):
158
if self._lockmode not in ['r', 'w']:
159
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
161
def _need_writelock(self):
162
if self._lockmode not in ['w']:
163
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
261
166
def abspath(self, name):
262
167
"""Return absolute filename for something in the branch"""
263
168
return os.path.join(self.base, name)
265
171
def relpath(self, path):
266
172
"""Return path relative to this branch of something inside it.
268
174
Raises an error if path is not in this branch."""
269
return _relpath(self.base, path)
175
rp = os.path.realpath(path)
177
if not rp.startswith(self.base):
178
bailout("path %r is not within branch %r" % (rp, self.base))
179
rp = rp[len(self.base):]
180
rp = rp.lstrip(os.sep)
271
184
def controlfilename(self, file_or_path):
272
185
"""Return location relative to branch."""
273
if isinstance(file_or_path, basestring):
186
if isinstance(file_or_path, types.StringTypes):
274
187
file_or_path = [file_or_path]
275
188
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
337
242
fmt = self.controlfile('branch-format', 'r').read()
338
243
fmt.replace('\r\n', '')
339
244
if fmt != BZR_BRANCH_FORMAT:
340
raise BzrError('sorry, branch format %r not supported' % fmt,
341
['use a different bzr version',
342
'or remove the .bzr directory and "bzr init" again'])
344
def get_root_id(self):
345
"""Return the id of this branches root"""
346
inv = self.read_working_inventory()
347
return inv.root.file_id
349
def set_root_id(self, file_id):
350
inv = self.read_working_inventory()
351
orig_root_id = inv.root.file_id
352
del inv._byid[inv.root.file_id]
353
inv.root.file_id = file_id
354
inv._byid[inv.root.file_id] = inv.root
357
if entry.parent_id in (None, orig_root_id):
358
entry.parent_id = inv.root.file_id
359
self._write_inventory(inv)
245
bailout('sorry, branch format %r not supported' % fmt,
246
['use a different bzr version',
247
'or remove the .bzr directory and "bzr init" again'])
361
250
def read_working_inventory(self):
362
251
"""Read the working inventory."""
363
from bzrlib.inventory import Inventory
364
from bzrlib.xml import unpack_xml
365
from time import time
369
# ElementTree does its own conversion from UTF-8, so open in
371
inv = unpack_xml(Inventory,
372
self.controlfile('inventory', 'rb'))
373
mutter("loaded inventory of %d items in %f"
374
% (len(inv), time() - before))
252
self._need_readlock()
254
# ElementTree does its own conversion from UTF-8, so open in
256
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
257
mutter("loaded inventory of %d items in %f"
258
% (len(inv), time.time() - before))
380
262
def _write_inventory(self, inv):
381
263
"""Update the working inventory.
383
265
That is to say, the inventory describing changes underway, that
384
266
will be committed to the next revision.
386
from bzrlib.atomicfile import AtomicFile
387
from bzrlib.xml import pack_xml
391
f = AtomicFile(self.controlfilename('inventory'), 'wb')
268
self._need_writelock()
269
## TODO: factor out to atomicfile? is rename safe on windows?
270
## TODO: Maybe some kind of clean/dirty marker on inventory?
271
tmpfname = self.controlfilename('inventory.tmp')
272
tmpf = file(tmpfname, 'wb')
275
inv_fname = self.controlfilename('inventory')
276
if sys.platform == 'win32':
278
os.rename(tmpfname, inv_fname)
400
279
mutter('wrote working inventory')
403
282
inventory = property(read_working_inventory, _write_inventory, None,
404
283
"""Inventory for the working copy.""")
407
def add(self, files, ids=None):
286
def add(self, files, verbose=False):
408
287
"""Make files versioned.
410
Note that the command line normally calls smart_add instead,
411
which can automatically recurse.
289
Note that the command line normally calls smart_add instead.
413
291
This puts the files in the Added state, so that they will be
414
292
recorded by the next commit.
417
List of paths to add, relative to the base of the tree.
420
If set, use these instead of automatically generated ids.
421
Must be the same length as the list of files, but may
422
contain None for ids that are to be autogenerated.
424
294
TODO: Perhaps have an option to add the ids even if the files do
427
TODO: Perhaps yield the ids and paths as they're added.
297
TODO: Perhaps return the ids of the files? But then again it
298
is easy to retrieve them if they're needed.
300
TODO: Option to specify file id.
302
TODO: Adding a directory should optionally recurse down and
303
add all non-ignored children. Perhaps do that in a
306
>>> b = ScratchBranch(files=['foo'])
307
>>> 'foo' in b.unknowns()
312
>>> 'foo' in b.unknowns()
314
>>> bool(b.inventory.path2id('foo'))
320
Traceback (most recent call last):
322
BzrError: ('foo is already versioned', [])
324
>>> b.add(['nothere'])
325
Traceback (most recent call last):
326
BzrError: ('cannot add: not a regular file or directory: nothere', [])
328
self._need_writelock()
429
330
# TODO: Re-adding a file that is removed in the working copy
430
331
# should probably put it back with the previous ID.
431
if isinstance(files, basestring):
432
assert(ids is None or isinstance(ids, basestring))
332
if isinstance(files, types.StringTypes):
438
ids = [None] * len(files)
440
assert(len(ids) == len(files))
444
inv = self.read_working_inventory()
445
for f,file_id in zip(files, ids):
446
if is_control_file(f):
447
raise BzrError("cannot add control file %s" % quotefn(f))
452
raise BzrError("cannot add top-level %r" % f)
454
fullpath = os.path.normpath(self.abspath(f))
457
kind = file_kind(fullpath)
459
# maybe something better?
460
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
462
if kind != 'file' and kind != 'directory':
463
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
466
file_id = gen_file_id(f)
467
inv.add_path(f, kind=kind, file_id=file_id)
469
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
471
self._write_inventory(inv)
335
inv = self.read_working_inventory()
337
if is_control_file(f):
338
bailout("cannot add control file %s" % quotefn(f))
343
bailout("cannot add top-level %r" % f)
345
fullpath = os.path.normpath(self.abspath(f))
348
kind = file_kind(fullpath)
350
# maybe something better?
351
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
353
if kind != 'file' and kind != 'directory':
354
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
356
file_id = gen_file_id(f)
357
inv.add_path(f, kind=kind, file_id=file_id)
360
show_status('A', kind, quotefn(f))
362
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
364
self._write_inventory(inv)
476
367
def print_file(self, file, revno):
477
368
"""Print `file` to stdout."""
480
tree = self.revision_tree(self.lookup_revision(revno))
481
# use inventory as it was in that revision
482
file_id = tree.inventory.path2id(file)
484
raise BzrError("%r is not present in revision %s" % (file, revno))
485
tree.print_file(file_id)
369
self._need_readlock()
370
tree = self.revision_tree(self.lookup_revision(revno))
371
# use inventory as it was in that revision
372
file_id = tree.inventory.path2id(file)
374
bailout("%r is not present in revision %d" % (file, revno))
375
tree.print_file(file_id)
490
378
def remove(self, files, verbose=False):
491
379
"""Mark nominated files for removal from the inventory.
563
458
return self.working_tree().unknowns()
566
def append_revision(self, *revision_ids):
567
from bzrlib.atomicfile import AtomicFile
569
for revision_id in revision_ids:
570
mutter("add {%s} to revision-history" % revision_id)
461
def commit(self, message, timestamp=None, timezone=None,
464
"""Commit working copy as a new revision.
466
The basic approach is to add all the file texts into the
467
store, then the inventory, then make a new revision pointing
468
to that inventory and store that.
470
This is not quite safe if the working copy changes during the
471
commit; for the moment that is simply not allowed. A better
472
approach is to make a temporary copy of the files before
473
computing their hashes, and then add those hashes in turn to
474
the inventory. This should mean at least that there are no
475
broken hash pointers. There is no way we can get a snapshot
476
of the whole directory at an instant. This would also have to
477
be robust against files disappearing, moving, etc. So the
478
whole thing is a bit hard.
480
timestamp -- if not None, seconds-since-epoch for a
481
postdated/predated commit.
483
self._need_writelock()
485
## TODO: Show branch names
487
# TODO: Don't commit if there are no changes, unless forced?
489
# First walk over the working inventory; and both update that
490
# and also build a new revision inventory. The revision
491
# inventory needs to hold the text-id, sha1 and size of the
492
# actual file versions committed in the revision. (These are
493
# not present in the working inventory.) We also need to
494
# detect missing/deleted files, and remove them from the
497
work_inv = self.read_working_inventory()
499
basis = self.basis_tree()
500
basis_inv = basis.inventory
502
for path, entry in work_inv.iter_entries():
503
## TODO: Cope with files that have gone missing.
505
## TODO: Check that the file kind has not changed from the previous
506
## revision of this file (if any).
510
p = self.abspath(path)
511
file_id = entry.file_id
512
mutter('commit prep file %s, id %r ' % (p, file_id))
514
if not os.path.exists(p):
515
mutter(" file is missing, removing from inventory")
517
show_status('D', entry.kind, quotefn(path))
518
missing_ids.append(file_id)
521
# TODO: Handle files that have been deleted
523
# TODO: Maybe a special case for empty files? Seems a
524
# waste to store them many times.
528
if basis_inv.has_id(file_id):
529
old_kind = basis_inv[file_id].kind
530
if old_kind != entry.kind:
531
bailout("entry %r changed kind from %r to %r"
532
% (file_id, old_kind, entry.kind))
534
if entry.kind == 'directory':
536
bailout("%s is entered as directory but not a directory" % quotefn(p))
537
elif entry.kind == 'file':
539
bailout("%s is entered as file but is not a file" % quotefn(p))
541
content = file(p, 'rb').read()
543
entry.text_sha1 = sha_string(content)
544
entry.text_size = len(content)
546
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
548
and (old_ie.text_size == entry.text_size)
549
and (old_ie.text_sha1 == entry.text_sha1)):
550
## assert content == basis.get_file(file_id).read()
551
entry.text_id = basis_inv[file_id].text_id
552
mutter(' unchanged from previous text_id {%s}' %
556
entry.text_id = gen_file_id(entry.name)
557
self.text_store.add(content, entry.text_id)
558
mutter(' stored with text_id {%s}' % entry.text_id)
562
elif (old_ie.name == entry.name
563
and old_ie.parent_id == entry.parent_id):
568
show_status(state, entry.kind, quotefn(path))
570
for file_id in missing_ids:
571
# have to do this later so we don't mess up the iterator.
572
# since parents may be removed before their children we
575
# FIXME: There's probably a better way to do this; perhaps
576
# the workingtree should know how to filter itself.
577
if work_inv.has_id(file_id):
578
del work_inv[file_id]
581
inv_id = rev_id = _gen_revision_id(time.time())
583
inv_tmp = tempfile.TemporaryFile()
584
inv.write_xml(inv_tmp)
586
self.inventory_store.add(inv_tmp, inv_id)
587
mutter('new inventory_id is {%s}' % inv_id)
589
self._write_inventory(work_inv)
591
if timestamp == None:
592
timestamp = time.time()
594
if committer == None:
595
committer = username()
598
timezone = local_time_offset()
600
mutter("building commit log message")
601
rev = Revision(timestamp=timestamp,
604
precursor = self.last_patch(),
609
rev_tmp = tempfile.TemporaryFile()
610
rev.write_xml(rev_tmp)
612
self.revision_store.add(rev_tmp, rev_id)
613
mutter("new revision_id is {%s}" % rev_id)
615
## XXX: Everything up to here can simply be orphaned if we abort
616
## the commit; it will leave junk files behind but that doesn't
619
## TODO: Read back the just-generated changeset, and make sure it
620
## applies and recreates the right state.
622
## TODO: Also calculate and store the inventory SHA1
623
mutter("committing patch r%d" % (self.revno() + 1))
626
self.append_revision(rev_id)
629
note("commited r%d" % self.revno())
632
def append_revision(self, revision_id):
633
mutter("add {%s} to revision-history" % revision_id)
572
634
rev_history = self.revision_history()
573
rev_history.extend(revision_ids)
575
f = AtomicFile(self.controlfilename('revision-history'))
577
for rev_id in rev_history:
584
def get_revision_xml(self, revision_id):
585
"""Return XML file object for revision object."""
586
if not revision_id or not isinstance(revision_id, basestring):
587
raise InvalidRevisionId(revision_id)
592
return self.revision_store[revision_id]
594
raise bzrlib.errors.NoSuchRevision(self, revision_id)
636
tmprhname = self.controlfilename('revision-history.tmp')
637
rhname = self.controlfilename('revision-history')
639
f = file(tmprhname, 'wt')
640
rev_history.append(revision_id)
641
f.write('\n'.join(rev_history))
645
if sys.platform == 'win32':
647
os.rename(tmprhname, rhname)
599
651
def get_revision(self, revision_id):
600
652
"""Return the Revision object for a named revision"""
601
xml_file = self.get_revision_xml(revision_id)
604
r = unpack_xml(Revision, xml_file)
605
except SyntaxError, e:
606
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
653
self._need_readlock()
654
r = Revision.read_xml(self.revision_store[revision_id])
610
655
assert r.revision_id == revision_id
614
def get_revision_delta(self, revno):
615
"""Return the delta for one revision.
617
The delta is relative to its mainline predecessor, or the
618
empty tree for revision 1.
620
assert isinstance(revno, int)
621
rh = self.revision_history()
622
if not (1 <= revno <= len(rh)):
623
raise InvalidRevisionNumber(revno)
625
# revno is 1-based; list is 0-based
627
new_tree = self.revision_tree(rh[revno-1])
629
old_tree = EmptyTree()
631
old_tree = self.revision_tree(rh[revno-2])
633
return compare_trees(old_tree, new_tree)
637
def get_revision_sha1(self, revision_id):
638
"""Hash the stored value of a revision, and return it."""
639
# In the future, revision entries will be signed. At that
640
# point, it is probably best *not* to include the signature
641
# in the revision hash. Because that lets you re-sign
642
# the revision, (add signatures/remove signatures) and still
643
# have all hash pointers stay consistent.
644
# But for now, just hash the contents.
645
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
648
659
def get_inventory(self, inventory_id):
649
660
"""Get Inventory object by hash.
651
662
TODO: Perhaps for this and similar methods, take a revision
652
663
parameter which can be either an integer revno or a
654
from bzrlib.inventory import Inventory
655
from bzrlib.xml import unpack_xml
657
return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
660
def get_inventory_xml(self, inventory_id):
661
"""Get inventory XML as a file object."""
662
return self.inventory_store[inventory_id]
665
def get_inventory_sha1(self, inventory_id):
666
"""Return the sha1 hash of the inventory entry
668
return sha_file(self.get_inventory_xml(inventory_id))
665
self._need_readlock()
666
i = Inventory.read_xml(self.inventory_store[inventory_id])
671
670
def get_revision_inventory(self, revision_id):
672
671
"""Return inventory of a past revision."""
673
# bzr 0.0.6 imposes the constraint that the inventory_id
674
# must be the same as its revision, so this is trivial.
672
self._need_readlock()
675
673
if revision_id == None:
676
from bzrlib.inventory import Inventory
677
return Inventory(self.get_root_id())
679
return self.get_inventory(revision_id)
676
return self.get_inventory(self.get_revision(revision_id).inventory_id)
682
679
def revision_history(self):
744
692
That is equivalent to the number of revisions committed to
695
>>> b = ScratchBranch()
698
>>> b.commit('no foo')
747
702
return len(self.revision_history())
750
705
def last_patch(self):
751
706
"""Return last patch hash, or None if no history.
708
>>> ScratchBranch().last_patch() == None
753
711
ph = self.revision_history()
760
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
762
If self and other have not diverged, return a list of the revisions
763
present in other, but missing from self.
765
>>> from bzrlib.commit import commit
766
>>> bzrlib.trace.silent = True
767
>>> br1 = ScratchBranch()
768
>>> br2 = ScratchBranch()
769
>>> br1.missing_revisions(br2)
771
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
772
>>> br1.missing_revisions(br2)
774
>>> br2.missing_revisions(br1)
776
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
777
>>> br1.missing_revisions(br2)
779
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
780
>>> br1.missing_revisions(br2)
782
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
783
>>> br1.missing_revisions(br2)
784
Traceback (most recent call last):
785
DivergedBranches: These branches have diverged.
787
self_history = self.revision_history()
788
self_len = len(self_history)
789
other_history = other.revision_history()
790
other_len = len(other_history)
791
common_index = min(self_len, other_len) -1
792
if common_index >= 0 and \
793
self_history[common_index] != other_history[common_index]:
794
raise DivergedBranches(self, other)
796
if stop_revision is None:
797
stop_revision = other_len
798
elif stop_revision > other_len:
799
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
801
return other_history[self_len:stop_revision]
804
def update_revisions(self, other, stop_revision=None):
805
"""Pull in all new revisions from other branch.
807
from bzrlib.fetch import greedy_fetch
809
pb = bzrlib.ui.ui_factory.progress_bar()
810
pb.update('comparing histories')
812
revision_ids = self.missing_revisions(other, stop_revision)
814
if len(revision_ids) > 0:
815
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
818
self.append_revision(*revision_ids)
819
## note("Added %d revisions." % count)
822
def install_revisions(self, other, revision_ids, pb):
823
if hasattr(other.revision_store, "prefetch"):
824
other.revision_store.prefetch(revision_ids)
825
if hasattr(other.inventory_store, "prefetch"):
826
inventory_ids = [other.get_revision(r).inventory_id
827
for r in revision_ids]
828
other.inventory_store.prefetch(inventory_ids)
831
pb = bzrlib.ui.ui_factory.progress_bar()
838
for i, rev_id in enumerate(revision_ids):
839
pb.update('fetching revision', i+1, len(revision_ids))
841
rev = other.get_revision(rev_id)
842
except bzrlib.errors.NoSuchRevision:
846
revisions.append(rev)
847
inv = other.get_inventory(str(rev.inventory_id))
848
for key, entry in inv.iter_entries():
849
if entry.text_id is None:
851
if entry.text_id not in self.text_store:
852
needed_texts.add(entry.text_id)
856
count, cp_fail = self.text_store.copy_multi(other.text_store,
858
#print "Added %d texts." % count
859
inventory_ids = [ f.inventory_id for f in revisions ]
860
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
862
#print "Added %d inventories." % count
863
revision_ids = [ f.revision_id for f in revisions]
865
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
868
assert len(cp_fail) == 0
869
return count, failures
872
def commit(self, *args, **kw):
873
from bzrlib.commit import commit
874
commit(self, *args, **kw)
877
def lookup_revision(self, revision):
878
"""Return the revision identifier for a given revision information."""
879
revno, info = self.get_revision_info(revision)
883
def revision_id_to_revno(self, revision_id):
884
"""Given a revision id, return its revno"""
885
history = self.revision_history()
887
return history.index(revision_id) + 1
889
raise bzrlib.errors.NoSuchRevision(self, revision_id)
892
def get_revision_info(self, revision):
893
"""Return (revno, revision id) for revision identifier.
895
revision can be an integer, in which case it is assumed to be revno (though
896
this will translate negative values into positive ones)
897
revision can also be a string, in which case it is parsed for something like
898
'date:' or 'revid:' etc.
903
try:# Convert to int if possible
904
revision = int(revision)
907
revs = self.revision_history()
908
if isinstance(revision, int):
911
# Mabye we should do this first, but we don't need it if revision == 0
913
revno = len(revs) + revision + 1
916
elif isinstance(revision, basestring):
917
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
918
if revision.startswith(prefix):
919
revno = func(self, revs, revision)
922
raise BzrError('No namespace registered for string: %r' % revision)
924
if revno is None or revno <= 0 or revno > len(revs):
925
raise BzrError("no such revision %s" % revision)
926
return revno, revs[revno-1]
928
def _namespace_revno(self, revs, revision):
929
"""Lookup a revision by revision number"""
930
assert revision.startswith('revno:')
932
return int(revision[6:])
935
REVISION_NAMESPACES['revno:'] = _namespace_revno
937
def _namespace_revid(self, revs, revision):
938
assert revision.startswith('revid:')
940
return revs.index(revision[6:]) + 1
943
REVISION_NAMESPACES['revid:'] = _namespace_revid
945
def _namespace_last(self, revs, revision):
946
assert revision.startswith('last:')
948
offset = int(revision[5:])
953
raise BzrError('You must supply a positive value for --revision last:XXX')
954
return len(revs) - offset + 1
955
REVISION_NAMESPACES['last:'] = _namespace_last
957
def _namespace_tag(self, revs, revision):
958
assert revision.startswith('tag:')
959
raise BzrError('tag: namespace registered, but not implemented.')
960
REVISION_NAMESPACES['tag:'] = _namespace_tag
962
def _namespace_date(self, revs, revision):
963
assert revision.startswith('date:')
965
# Spec for date revisions:
967
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
968
# it can also start with a '+/-/='. '+' says match the first
969
# entry after the given date. '-' is match the first entry before the date
970
# '=' is match the first entry after, but still on the given date.
972
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
973
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
974
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
975
# May 13th, 2005 at 0:00
977
# So the proper way of saying 'give me all entries for today' is:
978
# -r {date:+today}:{date:-tomorrow}
979
# The default is '=' when not supplied
982
if val[:1] in ('+', '-', '='):
983
match_style = val[:1]
986
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
987
if val.lower() == 'yesterday':
988
dt = today - datetime.timedelta(days=1)
989
elif val.lower() == 'today':
991
elif val.lower() == 'tomorrow':
992
dt = today + datetime.timedelta(days=1)
995
# This should be done outside the function to avoid recompiling it.
996
_date_re = re.compile(
997
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
999
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1001
m = _date_re.match(val)
1002
if not m or (not m.group('date') and not m.group('time')):
1003
raise BzrError('Invalid revision date %r' % revision)
1006
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1008
year, month, day = today.year, today.month, today.day
1010
hour = int(m.group('hour'))
1011
minute = int(m.group('minute'))
1012
if m.group('second'):
1013
second = int(m.group('second'))
1017
hour, minute, second = 0,0,0
1019
dt = datetime.datetime(year=year, month=month, day=day,
1020
hour=hour, minute=minute, second=second)
1024
if match_style == '-':
1026
elif match_style == '=':
1027
last = dt + datetime.timedelta(days=1)
1030
for i in range(len(revs)-1, -1, -1):
1031
r = self.get_revision(revs[i])
1032
# TODO: Handle timezone.
1033
dt = datetime.datetime.fromtimestamp(r.timestamp)
1034
if first >= dt and (last is None or dt >= last):
1037
for i in range(len(revs)):
1038
r = self.get_revision(revs[i])
1039
# TODO: Handle timezone.
1040
dt = datetime.datetime.fromtimestamp(r.timestamp)
1041
if first <= dt and (last is None or dt <= last):
1043
REVISION_NAMESPACES['date:'] = _namespace_date
718
def lookup_revision(self, revno):
719
"""Return revision hash for revision number."""
724
# list is 0-based; revisions are 1-based
725
return self.revision_history()[revno-1]
727
raise BzrError("no such revision %s" % revno)
1045
730
def revision_tree(self, revision_id):
1046
731
"""Return Tree for a revision on this branch.
1048
733
`revision_id` may be None for the null revision, in which case
1049
734
an `EmptyTree` is returned."""
1050
# TODO: refactor this to use an existing revision object
1051
# so we don't need to read it in twice.
735
self._need_readlock()
1052
736
if revision_id == None:
1053
737
return EmptyTree()
771
def write_log(self, show_timezone='original', verbose=False):
772
"""Write out human-readable log of commits to this branch
774
utc -- If true, show dates in universal time, not local time."""
775
self._need_readlock()
776
## TODO: Option to choose either original, utc or local timezone
779
for p in self.revision_history():
781
print 'revno:', revno
782
## TODO: Show hash if --id is given.
783
##print 'revision-hash:', p
784
rev = self.get_revision(p)
785
print 'committer:', rev.committer
786
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
789
## opportunistic consistency check, same as check_patch_chaining
790
if rev.precursor != precursor:
791
bailout("mismatched precursor!")
795
print ' (no message)'
797
for l in rev.message.split('\n'):
800
if verbose == True and precursor != None:
801
print 'changed files:'
802
tree = self.revision_tree(p)
803
prevtree = self.revision_tree(precursor)
805
for file_state, fid, old_name, new_name, kind in \
806
diff_trees(prevtree, tree, ):
807
if file_state == 'A' or file_state == 'M':
808
show_status(file_state, kind, new_name)
809
elif file_state == 'D':
810
show_status(file_state, kind, old_name)
811
elif file_state == 'R':
812
show_status(file_state, kind,
813
old_name + ' => ' + new_name)
1078
819
def rename_one(self, from_rel, to_rel):
1079
820
"""Rename one file.
1081
822
This can change the directory or the filename or both.
824
self._need_writelock()
825
tree = self.working_tree()
827
if not tree.has_filename(from_rel):
828
bailout("can't rename: old working file %r does not exist" % from_rel)
829
if tree.has_filename(to_rel):
830
bailout("can't rename: new working file %r already exists" % to_rel)
832
file_id = inv.path2id(from_rel)
834
bailout("can't rename: old name %r is not versioned" % from_rel)
836
if inv.path2id(to_rel):
837
bailout("can't rename: new name %r is already versioned" % to_rel)
839
to_dir, to_tail = os.path.split(to_rel)
840
to_dir_id = inv.path2id(to_dir)
841
if to_dir_id == None and to_dir != '':
842
bailout("can't determine destination directory id for %r" % to_dir)
844
mutter("rename_one:")
845
mutter(" file_id {%s}" % file_id)
846
mutter(" from_rel %r" % from_rel)
847
mutter(" to_rel %r" % to_rel)
848
mutter(" to_dir %r" % to_dir)
849
mutter(" to_dir_id {%s}" % to_dir_id)
851
inv.rename(file_id, to_dir_id, to_tail)
853
print "%s => %s" % (from_rel, to_rel)
855
from_abs = self.abspath(from_rel)
856
to_abs = self.abspath(to_rel)
1085
tree = self.working_tree()
1086
inv = tree.inventory
1087
if not tree.has_filename(from_rel):
1088
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1089
if tree.has_filename(to_rel):
1090
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1092
file_id = inv.path2id(from_rel)
1094
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1096
if inv.path2id(to_rel):
1097
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1099
to_dir, to_tail = os.path.split(to_rel)
1100
to_dir_id = inv.path2id(to_dir)
1101
if to_dir_id == None and to_dir != '':
1102
raise BzrError("can't determine destination directory id for %r" % to_dir)
1104
mutter("rename_one:")
1105
mutter(" file_id {%s}" % file_id)
1106
mutter(" from_rel %r" % from_rel)
1107
mutter(" to_rel %r" % to_rel)
1108
mutter(" to_dir %r" % to_dir)
1109
mutter(" to_dir_id {%s}" % to_dir_id)
1111
inv.rename(file_id, to_dir_id, to_tail)
1113
from_abs = self.abspath(from_rel)
1114
to_abs = self.abspath(to_rel)
1116
os.rename(from_abs, to_abs)
1118
raise BzrError("failed to rename %r to %r: %s"
1119
% (from_abs, to_abs, e[1]),
1120
["rename rolled back"])
1122
self._write_inventory(inv)
858
os.rename(from_abs, to_abs)
860
bailout("failed to rename %r to %r: %s"
861
% (from_abs, to_abs, e[1]),
862
["rename rolled back"])
864
self._write_inventory(inv)
1127
868
def move(self, from_paths, to_name):
1135
876
Note that to_name is only the last component of the new name;
1136
877
this doesn't change the directory.
1138
This returns a list of (from_path, to_path) pairs for each
1139
entry that is moved.
1144
## TODO: Option to move IDs only
1145
assert not isinstance(from_paths, basestring)
1146
tree = self.working_tree()
1147
inv = tree.inventory
1148
to_abs = self.abspath(to_name)
1149
if not isdir(to_abs):
1150
raise BzrError("destination %r is not a directory" % to_abs)
1151
if not tree.has_filename(to_name):
1152
raise BzrError("destination %r not in working directory" % to_abs)
1153
to_dir_id = inv.path2id(to_name)
1154
if to_dir_id == None and to_name != '':
1155
raise BzrError("destination %r is not a versioned directory" % to_name)
1156
to_dir_ie = inv[to_dir_id]
1157
if to_dir_ie.kind not in ('directory', 'root_directory'):
1158
raise BzrError("destination %r is not a directory" % to_abs)
1160
to_idpath = inv.get_idpath(to_dir_id)
1162
for f in from_paths:
1163
if not tree.has_filename(f):
1164
raise BzrError("%r does not exist in working tree" % f)
1165
f_id = inv.path2id(f)
1167
raise BzrError("%r is not versioned" % f)
1168
name_tail = splitpath(f)[-1]
1169
dest_path = appendpath(to_name, name_tail)
1170
if tree.has_filename(dest_path):
1171
raise BzrError("destination %r already exists" % dest_path)
1172
if f_id in to_idpath:
1173
raise BzrError("can't move %r to a subdirectory of itself" % f)
1175
# OK, so there's a race here, it's possible that someone will
1176
# create a file in this interval and then the rename might be
1177
# left half-done. But we should have caught most problems.
1179
for f in from_paths:
1180
name_tail = splitpath(f)[-1]
1181
dest_path = appendpath(to_name, name_tail)
1182
result.append((f, dest_path))
1183
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1185
os.rename(self.abspath(f), self.abspath(dest_path))
1187
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1188
["rename rolled back"])
1190
self._write_inventory(inv)
1197
def revert(self, filenames, old_tree=None, backups=True):
1198
"""Restore selected files to the versions from a previous tree.
1201
If true (default) backups are made of files before
1204
from bzrlib.errors import NotVersionedError, BzrError
1205
from bzrlib.atomicfile import AtomicFile
1206
from bzrlib.osutils import backup_file
879
self._need_writelock()
880
## TODO: Option to move IDs only
881
assert not isinstance(from_paths, basestring)
882
tree = self.working_tree()
884
to_abs = self.abspath(to_name)
885
if not isdir(to_abs):
886
bailout("destination %r is not a directory" % to_abs)
887
if not tree.has_filename(to_name):
888
bailout("destination %r not in working directory" % to_abs)
889
to_dir_id = inv.path2id(to_name)
890
if to_dir_id == None and to_name != '':
891
bailout("destination %r is not a versioned directory" % to_name)
892
to_dir_ie = inv[to_dir_id]
893
if to_dir_ie.kind not in ('directory', 'root_directory'):
894
bailout("destination %r is not a directory" % to_abs)
896
to_idpath = Set(inv.get_idpath(to_dir_id))
899
if not tree.has_filename(f):
900
bailout("%r does not exist in working tree" % f)
901
f_id = inv.path2id(f)
903
bailout("%r is not versioned" % f)
904
name_tail = splitpath(f)[-1]
905
dest_path = appendpath(to_name, name_tail)
906
if tree.has_filename(dest_path):
907
bailout("destination %r already exists" % dest_path)
908
if f_id in to_idpath:
909
bailout("can't move %r to a subdirectory of itself" % f)
911
# OK, so there's a race here, it's possible that someone will
912
# create a file in this interval and then the rename might be
913
# left half-done. But we should have caught most problems.
916
name_tail = splitpath(f)[-1]
917
dest_path = appendpath(to_name, name_tail)
918
print "%s => %s" % (f, dest_path)
919
inv.rename(inv.path2id(f), to_dir_id, name_tail)
921
os.rename(self.abspath(f), self.abspath(dest_path))
923
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
924
["rename rolled back"])
926
self._write_inventory(inv)
930
def show_status(self, show_all=False):
931
"""Display single-line status for non-ignored working files.
933
The list is show sorted in order by file name.
935
>>> b = ScratchBranch(files=['foo', 'foo~'])
941
>>> b.commit("add foo")
943
>>> os.unlink(b.abspath('foo'))
1208
inv = self.read_working_inventory()
1209
if old_tree is None:
1210
old_tree = self.basis_tree()
1211
old_inv = old_tree.inventory
1214
for fn in filenames:
1215
file_id = inv.path2id(fn)
1217
raise NotVersionedError("not a versioned file", fn)
1218
if not old_inv.has_id(file_id):
1219
raise BzrError("file not present in old tree", fn, file_id)
1220
nids.append((fn, file_id))
1222
# TODO: Rename back if it was previously at a different location
1224
# TODO: If given a directory, restore the entire contents from
1225
# the previous version.
1227
# TODO: Make a backup to a temporary file.
1229
# TODO: If the file previously didn't exist, delete it?
1230
for fn, file_id in nids:
1233
f = AtomicFile(fn, 'wb')
1235
f.write(old_tree.get_file(file_id).read())
1241
def pending_merges(self):
1242
"""Return a list of pending merges.
1244
These are revisions that have been merged into the working
1245
directory but not yet committed.
947
TODO: Get state for single files.
1247
cfn = self.controlfilename('pending-merges')
1248
if not os.path.exists(cfn):
1251
for l in self.controlfile('pending-merges', 'r').readlines():
1252
p.append(l.rstrip('\n'))
1256
def add_pending_merge(self, revision_id):
1257
from bzrlib.revision import validate_revision_id
1259
validate_revision_id(revision_id)
1261
p = self.pending_merges()
1262
if revision_id in p:
1264
p.append(revision_id)
1265
self.set_pending_merges(p)
1268
def set_pending_merges(self, rev_list):
1269
from bzrlib.atomicfile import AtomicFile
1272
f = AtomicFile(self.controlfilename('pending-merges'))
949
self._need_readlock()
951
# We have to build everything into a list first so that it can
952
# sorted by name, incorporating all the different sources.
954
# FIXME: Rather than getting things in random order and then sorting,
955
# just step through in order.
957
# Interesting case: the old ID for a file has been removed,
958
# but a new file has been created under that name.
960
old = self.basis_tree()
961
new = self.working_tree()
963
for fs, fid, oldname, newname, kind in diff_trees(old, new):
965
show_status(fs, kind,
966
oldname + ' => ' + newname)
967
elif fs == 'A' or fs == 'M':
968
show_status(fs, kind, newname)
970
show_status(fs, kind, oldname)
973
show_status(fs, kind, newname)
976
show_status(fs, kind, newname)
978
show_status(fs, kind, newname)
980
bailout("weird file state %r" % ((fs, fid),))
1284
984
class ScratchBranch(Branch):