15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
import traceback, socket, fnmatch, difflib, time
20
from binascii import hexlify
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
23
from inventory import Inventory
24
from trace import mutter, note
25
from tree import Tree, EmptyTree, RevisionTree
26
from inventory import InventoryEntry, Inventory
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
joinpath, sha_string, file_kind, local_time_offset, appendpath
30
from store import ImmutableStore
31
from revision import Revision
32
from errors import BzrError
33
from textui import show_status
26
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
36
## TODO: Maybe include checks for common corruption of newlines, etc?
36
45
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
49
def _relpath(base, path):
57
50
"""Return path relative to base, or raise exception.
117
110
self.branch2 = branch2
118
111
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
113
######################################################################
258
241
def controlfilename(self, file_or_path):
259
242
"""Return location relative to branch."""
260
if isinstance(file_or_path, basestring):
243
if isinstance(file_or_path, types.StringTypes):
261
244
file_or_path = [file_or_path]
262
245
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
292
275
def _make_control(self):
293
from bzrlib.inventory import Inventory
294
from bzrlib.xml import pack_xml
296
276
os.mkdir(self.controlfilename([]))
297
277
self.controlfile('README', 'w').write(
298
278
"This is a Bazaar-NG control directory.\n"
299
"Do not change any files in this directory.\n")
279
"Do not change any files in this directory.")
300
280
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
301
281
for d in ('text-store', 'inventory-store', 'revision-store'):
302
282
os.mkdir(self.controlfilename(d))
306
286
self.controlfile(f, 'w').write('')
307
287
mutter('created control directory in ' + self.base)
309
pack_xml(Inventory(), self.controlfile('inventory','w'))
288
Inventory().write_xml(self.controlfile('inventory','w'))
312
291
def _check_format(self):
332
311
def read_working_inventory(self):
333
312
"""Read the working inventory."""
334
from bzrlib.inventory import Inventory
335
from bzrlib.xml import unpack_xml
336
from time import time
314
# ElementTree does its own conversion from UTF-8, so open in
340
# ElementTree does its own conversion from UTF-8, so open in
342
inv = unpack_xml(Inventory,
343
self.controlfile('inventory', 'rb'))
318
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
344
319
mutter("loaded inventory of %d items in %f"
345
% (len(inv), time() - before))
320
% (len(inv), time.time() - before))
354
329
That is to say, the inventory describing changes underway, that
355
330
will be committed to the next revision.
357
from bzrlib.atomicfile import AtomicFile
358
from bzrlib.xml import pack_xml
362
f = AtomicFile(self.controlfilename('inventory'), 'wb')
332
## TODO: factor out to atomicfile? is rename safe on windows?
333
## TODO: Maybe some kind of clean/dirty marker on inventory?
334
tmpfname = self.controlfilename('inventory.tmp')
335
tmpf = file(tmpfname, 'wb')
338
inv_fname = self.controlfilename('inventory')
339
if sys.platform == 'win32':
341
os.rename(tmpfname, inv_fname)
371
342
mutter('wrote working inventory')
401
372
add all non-ignored children. Perhaps do that in a
402
373
higher-level method.
404
from bzrlib.textui import show_status
405
375
# TODO: Re-adding a file that is removed in the working copy
406
376
# should probably put it back with the previous ID.
407
if isinstance(files, basestring):
408
assert(ids is None or isinstance(ids, basestring))
377
if isinstance(files, types.StringTypes):
378
assert(ids is None or isinstance(ids, types.StringTypes))
410
380
if ids is not None:
443
413
inv.add_path(f, kind=kind, file_id=file_id)
446
print 'added', quotefn(f)
416
show_status('A', kind, quotefn(f))
448
418
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
480
450
is the opposite of add. Removing it is consistent with most
481
451
other tools. Maybe an option.
483
from bzrlib.textui import show_status
484
453
## TODO: Normalize names
485
454
## TODO: Remove nested loops; better scalability
486
if isinstance(files, basestring):
455
if isinstance(files, types.StringTypes):
489
458
self.lock_write()
515
484
# FIXME: this doesn't need to be a branch method
516
485
def set_inventory(self, new_inventory_list):
517
from bzrlib.inventory import Inventory, InventoryEntry
518
486
inv = Inventory()
519
487
for path, file_id, parent, kind in new_inventory_list:
520
488
name = os.path.basename(path)
546
514
def append_revision(self, revision_id):
547
from bzrlib.atomicfile import AtomicFile
549
515
mutter("add {%s} to revision-history" % revision_id)
550
rev_history = self.revision_history() + [revision_id]
552
f = AtomicFile(self.controlfilename('revision-history'))
554
for rev_id in rev_history:
516
rev_history = self.revision_history()
518
tmprhname = self.controlfilename('revision-history.tmp')
519
rhname = self.controlfilename('revision-history')
521
f = file(tmprhname, 'wt')
522
rev_history.append(revision_id)
523
f.write('\n'.join(rev_history))
527
if sys.platform == 'win32':
529
os.rename(tmprhname, rhname)
561
533
def get_revision(self, revision_id):
562
534
"""Return the Revision object for a named revision"""
563
from bzrlib.revision import Revision
564
from bzrlib.xml import unpack_xml
568
if not revision_id or not isinstance(revision_id, basestring):
569
raise ValueError('invalid revision-id: %r' % revision_id)
570
r = unpack_xml(Revision, self.revision_store[revision_id])
535
r = Revision.read_xml(self.revision_store[revision_id])
574
536
assert r.revision_id == revision_id
578
def get_revision_sha1(self, revision_id):
579
"""Hash the stored value of a revision, and return it."""
580
# In the future, revision entries will be signed. At that
581
# point, it is probably best *not* to include the signature
582
# in the revision hash. Because that lets you re-sign
583
# the revision, (add signatures/remove signatures) and still
584
# have all hash pointers stay consistent.
585
# But for now, just hash the contents.
586
return sha_file(self.revision_store[revision_id])
589
540
def get_inventory(self, inventory_id):
592
543
TODO: Perhaps for this and similar methods, take a revision
593
544
parameter which can be either an integer revno or a
595
from bzrlib.inventory import Inventory
596
from bzrlib.xml import unpack_xml
598
return unpack_xml(Inventory, self.inventory_store[inventory_id])
601
def get_inventory_sha1(self, inventory_id):
602
"""Return the sha1 hash of the inventory entry
604
return sha_file(self.inventory_store[inventory_id])
546
i = Inventory.read_xml(self.inventory_store[inventory_id])
607
550
def get_revision_inventory(self, revision_id):
608
551
"""Return inventory of a past revision."""
609
552
if revision_id == None:
610
from bzrlib.inventory import Inventory
611
553
return Inventory()
613
555
return self.get_inventory(self.get_revision(revision_id).inventory_id)
715
def missing_revisions(self, other, stop_revision=None):
657
def missing_revisions(self, other):
717
659
If self and other have not diverged, return a list of the revisions
718
660
present in other, but missing from self.
747
689
if common_index >= 0 and \
748
690
self_history[common_index] != other_history[common_index]:
749
691
raise DivergedBranches(self, other)
751
if stop_revision is None:
752
stop_revision = other_len
753
elif stop_revision > other_len:
754
raise NoSuchRevision(self, stop_revision)
756
return other_history[self_len:stop_revision]
759
def update_revisions(self, other, stop_revision=None):
760
"""Pull in all new revisions from other branch.
692
if self_len < other_len:
693
return other_history[self_len:]
697
def update_revisions(self, other):
698
"""If self and other have not diverged, ensure self has all the
762
701
>>> from bzrlib.commit import commit
763
702
>>> bzrlib.trace.silent = True
764
703
>>> br1 = ScratchBranch(files=['foo', 'bar'])
779
718
>>> br1.text_store.total_size() == br2.text_store.total_size()
782
from bzrlib.progress import ProgressBar
786
from sets import Set as set
790
pb.update('comparing histories')
791
revision_ids = self.missing_revisions(other, stop_revision)
793
if hasattr(other.revision_store, "prefetch"):
794
other.revision_store.prefetch(revision_ids)
795
if hasattr(other.inventory_store, "prefetch"):
796
inventory_ids = [other.get_revision(r).inventory_id
797
for r in revision_ids]
798
other.inventory_store.prefetch(inventory_ids)
803
for rev_id in revision_ids:
805
pb.update('fetching revision', i, len(revision_ids))
806
rev = other.get_revision(rev_id)
807
revisions.append(rev)
721
revision_ids = self.missing_revisions(other)
722
revisions = [other.get_revision(f) for f in revision_ids]
723
needed_texts = sets.Set()
724
for rev in revisions:
808
725
inv = other.get_inventory(str(rev.inventory_id))
809
726
for key, entry in inv.iter_entries():
810
727
if entry.text_id is None:
812
729
if entry.text_id not in self.text_store:
813
730
needed_texts.add(entry.text_id)
817
731
count = self.text_store.copy_multi(other.text_store, needed_texts)
818
732
print "Added %d texts." % count
819
733
inventory_ids = [ f.inventory_id for f in revisions ]
851
766
`revision_id` may be None for the null revision, in which case
852
767
an `EmptyTree` is returned."""
853
from bzrlib.tree import EmptyTree, RevisionTree
854
768
# TODO: refactor this to use an existing revision object
855
769
# so we don't need to read it in twice.
856
770
if revision_id == None:
998
def revert(self, filenames, old_tree=None, backups=True):
999
"""Restore selected files to the versions from a previous tree.
1002
If true (default) backups are made of files before
1005
from bzrlib.errors import NotVersionedError, BzrError
1006
from bzrlib.atomicfile import AtomicFile
1007
from bzrlib.osutils import backup_file
1009
inv = self.read_working_inventory()
1010
if old_tree is None:
1011
old_tree = self.basis_tree()
1012
old_inv = old_tree.inventory
1015
for fn in filenames:
1016
file_id = inv.path2id(fn)
1018
raise NotVersionedError("not a versioned file", fn)
1019
if not old_inv.has_id(file_id):
1020
raise BzrError("file not present in old tree", fn, file_id)
1021
nids.append((fn, file_id))
1023
# TODO: Rename back if it was previously at a different location
1025
# TODO: If given a directory, restore the entire contents from
1026
# the previous version.
1028
# TODO: Make a backup to a temporary file.
1030
# TODO: If the file previously didn't exist, delete it?
1031
for fn, file_id in nids:
1034
f = AtomicFile(fn, 'wb')
1036
f.write(old_tree.get_file(file_id).read())
1043
912
class ScratchBranch(Branch):
1044
913
"""Special test class: a branch that cleans up after itself.
1059
928
If any files are listed, they are created in the working copy.
1061
from tempfile import mkdtemp
1063
931
if base is None:
932
base = tempfile.mkdtemp()
1066
934
Branch.__init__(self, base, init=init)
1080
948
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1083
from shutil import copytree
1084
from tempfile import mkdtemp
951
base = tempfile.mkdtemp()
1087
copytree(self.base, base, symlinks=True)
953
shutil.copytree(self.base, base, symlinks=True)
1088
954
return ScratchBranch(base=base)
1090
956
def __del__(self):
1093
959
def destroy(self):
1094
960
"""Destroy the test branch, removing the scratch directory."""
1095
from shutil import rmtree
1098
963
mutter("delete ScratchBranch %s" % self.base)
964
shutil.rmtree(self.base)
1100
965
except OSError, e:
1101
966
# Work around for shutil.rmtree failing on Windows when
1102
967
# readonly files are encountered
1154
1017
name = re.sub(r'[^\w.]', '', name)
1156
1019
s = hexlify(rand_bytes(8))
1157
return '-'.join((name, compact_date(time()), s))
1020
return '-'.join((name, compact_date(time.time()), s))