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
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_file, 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
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
35
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
27
## TODO: Maybe include checks for common corruption of newlines, etc?
45
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')
49
56
def _relpath(base, path):
50
57
"""Return path relative to base, or raise exception.
250
258
def controlfilename(self, file_or_path):
251
259
"""Return location relative to branch."""
252
if isinstance(file_or_path, types.StringTypes):
260
if isinstance(file_or_path, basestring):
253
261
file_or_path = [file_or_path]
254
262
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
284
292
def _make_control(self):
293
from bzrlib.inventory import Inventory
294
from bzrlib.xml import pack_xml
285
296
os.mkdir(self.controlfilename([]))
286
297
self.controlfile('README', 'w').write(
287
298
"This is a Bazaar-NG control directory.\n"
291
302
os.mkdir(self.controlfilename(d))
292
303
for f in ('revision-history', 'merged-patches',
293
304
'pending-merged-patches', 'branch-name',
295
307
self.controlfile(f, 'w').write('')
296
308
mutter('created control directory in ' + self.base)
297
Inventory().write_xml(self.controlfile('inventory','w'))
310
pack_xml(Inventory(), self.controlfile('inventory','w'))
300
313
def _check_format(self):
320
333
def read_working_inventory(self):
321
334
"""Read the working inventory."""
323
# ElementTree does its own conversion from UTF-8, so open in
335
from bzrlib.inventory import Inventory
336
from bzrlib.xml import unpack_xml
337
from time import time
327
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
341
# ElementTree does its own conversion from UTF-8, so open in
343
inv = unpack_xml(Inventory,
344
self.controlfile('inventory', 'rb'))
328
345
mutter("loaded inventory of %d items in %f"
329
% (len(inv), time.time() - before))
346
% (len(inv), time() - before))
338
355
That is to say, the inventory describing changes underway, that
339
356
will be committed to the next revision.
341
## TODO: factor out to atomicfile? is rename safe on windows?
342
## TODO: Maybe some kind of clean/dirty marker on inventory?
343
tmpfname = self.controlfilename('inventory.tmp')
344
tmpf = file(tmpfname, 'wb')
347
inv_fname = self.controlfilename('inventory')
348
if sys.platform == 'win32':
350
os.rename(tmpfname, inv_fname)
358
from bzrlib.atomicfile import AtomicFile
359
from bzrlib.xml import pack_xml
363
f = AtomicFile(self.controlfilename('inventory'), 'wb')
351
372
mutter('wrote working inventory')
381
402
add all non-ignored children. Perhaps do that in a
382
403
higher-level method.
405
from bzrlib.textui import show_status
384
406
# TODO: Re-adding a file that is removed in the working copy
385
407
# should probably put it back with the previous ID.
386
if isinstance(files, types.StringTypes):
387
assert(ids is None or isinstance(ids, types.StringTypes))
408
if isinstance(files, basestring):
409
assert(ids is None or isinstance(ids, basestring))
389
411
if ids is not None:
422
444
inv.add_path(f, kind=kind, file_id=file_id)
425
show_status('A', kind, quotefn(f))
447
print 'added', quotefn(f)
427
449
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
459
481
is the opposite of add. Removing it is consistent with most
460
482
other tools. Maybe an option.
484
from bzrlib.textui import show_status
462
485
## TODO: Normalize names
463
486
## TODO: Remove nested loops; better scalability
464
if isinstance(files, types.StringTypes):
487
if isinstance(files, basestring):
467
490
self.lock_write()
493
516
# FIXME: this doesn't need to be a branch method
494
517
def set_inventory(self, new_inventory_list):
518
from bzrlib.inventory import Inventory, InventoryEntry
495
519
inv = Inventory()
496
520
for path, file_id, parent, kind in new_inventory_list:
497
521
name = os.path.basename(path)
523
547
def append_revision(self, revision_id):
548
from bzrlib.atomicfile import AtomicFile
524
550
mutter("add {%s} to revision-history" % revision_id)
525
rev_history = self.revision_history()
527
tmprhname = self.controlfilename('revision-history.tmp')
528
rhname = self.controlfilename('revision-history')
530
f = file(tmprhname, 'wt')
531
rev_history.append(revision_id)
532
f.write('\n'.join(rev_history))
536
if sys.platform == 'win32':
538
os.rename(tmprhname, rhname)
551
rev_history = self.revision_history() + [revision_id]
553
f = AtomicFile(self.controlfilename('revision-history'))
555
for rev_id in rev_history:
542
562
def get_revision(self, revision_id):
543
563
"""Return the Revision object for a named revision"""
544
if not revision_id or not isinstance(revision_id, basestring):
545
raise ValueError('invalid revision-id: %r' % revision_id)
546
r = Revision.read_xml(self.revision_store[revision_id])
564
from bzrlib.revision import Revision
565
from bzrlib.xml import unpack_xml
569
if not revision_id or not isinstance(revision_id, basestring):
570
raise ValueError('invalid revision-id: %r' % revision_id)
571
r = unpack_xml(Revision, self.revision_store[revision_id])
547
575
assert r.revision_id == revision_id
550
579
def get_revision_sha1(self, revision_id):
551
580
"""Hash the stored value of a revision, and return it."""
564
593
TODO: Perhaps for this and similar methods, take a revision
565
594
parameter which can be either an integer revno or a
567
i = Inventory.read_xml(self.inventory_store[inventory_id])
596
from bzrlib.inventory import Inventory
597
from bzrlib.xml import unpack_xml
599
return unpack_xml(Inventory, self.inventory_store[inventory_id])
570
602
def get_inventory_sha1(self, inventory_id):
571
603
"""Return the sha1 hash of the inventory entry
576
608
def get_revision_inventory(self, revision_id):
577
609
"""Return inventory of a past revision."""
610
# bzr 0.0.6 imposes the constraint that the inventory_id
611
# must be the same as its revision, so this is trivial.
578
612
if revision_id == None:
613
from bzrlib.inventory import Inventory
579
614
return Inventory()
581
return self.get_inventory(self.get_revision(revision_id).inventory_id)
616
return self.get_inventory(revision_id)
584
619
def revision_history(self):
750
785
from bzrlib.progress import ProgressBar
789
from sets import Set as set
752
791
pb = ProgressBar()
754
793
pb.update('comparing histories')
755
794
revision_ids = self.missing_revisions(other, stop_revision)
796
if hasattr(other.revision_store, "prefetch"):
797
other.revision_store.prefetch(revision_ids)
798
if hasattr(other.inventory_store, "prefetch"):
799
inventory_ids = [other.get_revision(r).inventory_id
800
for r in revision_ids]
801
other.inventory_store.prefetch(inventory_ids)
757
needed_texts = sets.Set()
759
806
for rev_id in revision_ids:
808
854
`revision_id` may be None for the null revision, in which case
809
855
an `EmptyTree` is returned."""
856
from bzrlib.tree import EmptyTree, RevisionTree
810
857
# TODO: refactor this to use an existing revision object
811
858
# so we don't need to read it in twice.
812
859
if revision_id == None:
1001
def revert(self, filenames, old_tree=None, backups=True):
1002
"""Restore selected files to the versions from a previous tree.
1005
If true (default) backups are made of files before
1008
from bzrlib.errors import NotVersionedError, BzrError
1009
from bzrlib.atomicfile import AtomicFile
1010
from bzrlib.osutils import backup_file
1012
inv = self.read_working_inventory()
1013
if old_tree is None:
1014
old_tree = self.basis_tree()
1015
old_inv = old_tree.inventory
1018
for fn in filenames:
1019
file_id = inv.path2id(fn)
1021
raise NotVersionedError("not a versioned file", fn)
1022
if not old_inv.has_id(file_id):
1023
raise BzrError("file not present in old tree", fn, file_id)
1024
nids.append((fn, file_id))
1026
# TODO: Rename back if it was previously at a different location
1028
# TODO: If given a directory, restore the entire contents from
1029
# the previous version.
1031
# TODO: Make a backup to a temporary file.
1033
# TODO: If the file previously didn't exist, delete it?
1034
for fn, file_id in nids:
1037
f = AtomicFile(fn, 'wb')
1039
f.write(old_tree.get_file(file_id).read())
1045
def pending_merges(self):
1046
"""Return a list of pending merges.
1048
These are revisions that have been merged into the working
1049
directory but not yet committed.
1051
cfn = self.controlfilename('pending-merges')
1052
if not os.path.exists(cfn):
1055
for l in self.controlfile('pending-merges', 'r').readlines():
1056
p.append(l.rstrip('\n'))
1060
def add_pending_merge(self, revision_id):
1061
from bzrlib.revision import validate_revision_id
1063
validate_revision_id(revision_id)
1065
p = self.pending_merges()
1066
if revision_id in p:
1068
p.append(revision_id)
1069
self.set_pending_merges(p)
1072
def set_pending_merges(self, rev_list):
1073
from bzrlib.atomicfile import AtomicFile
1076
f = AtomicFile(self.controlfilename('pending-merges'))
954
1088
class ScratchBranch(Branch):
955
1089
"""Special test class: a branch that cleans up after itself.
970
1104
If any files are listed, they are created in the working copy.
1106
from tempfile import mkdtemp
973
1108
if base is None:
974
base = tempfile.mkdtemp()
976
1111
Branch.__init__(self, base, init=init)
990
1125
>>> os.path.isfile(os.path.join(clone.base, "file1"))
993
base = tempfile.mkdtemp()
1128
from shutil import copytree
1129
from tempfile import mkdtemp
995
shutil.copytree(self.base, base, symlinks=True)
1132
copytree(self.base, base, symlinks=True)
996
1133
return ScratchBranch(base=base)
998
1135
def __del__(self):
1001
1138
def destroy(self):
1002
1139
"""Destroy the test branch, removing the scratch directory."""
1140
from shutil import rmtree
1005
1143
mutter("delete ScratchBranch %s" % self.base)
1006
shutil.rmtree(self.base)
1007
1145
except OSError, e:
1008
1146
# Work around for shutil.rmtree failing on Windows when
1009
1147
# readonly files are encountered
1059
1199
name = re.sub(r'[^\w.]', '', name)
1061
1201
s = hexlify(rand_bytes(8))
1062
return '-'.join((name, compact_date(time.time()), s))
1202
return '-'.join((name, compact_date(time()), s))