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_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
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.
258
250
def controlfilename(self, file_or_path):
259
251
"""Return location relative to branch."""
260
if isinstance(file_or_path, basestring):
252
if isinstance(file_or_path, types.StringTypes):
261
253
file_or_path = [file_or_path]
262
254
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
292
284
def _make_control(self):
293
from bzrlib.inventory import Inventory
294
from bzrlib.xml import pack_xml
296
285
os.mkdir(self.controlfilename([]))
297
286
self.controlfile('README', 'w').write(
298
287
"This is a Bazaar-NG control directory.\n"
302
291
os.mkdir(self.controlfilename(d))
303
292
for f in ('revision-history', 'merged-patches',
304
293
'pending-merged-patches', 'branch-name',
307
295
self.controlfile(f, 'w').write('')
308
296
mutter('created control directory in ' + self.base)
310
pack_xml(Inventory(), self.controlfile('inventory','w'))
297
Inventory().write_xml(self.controlfile('inventory','w'))
313
300
def _check_format(self):
333
320
def read_working_inventory(self):
334
321
"""Read the working inventory."""
335
from bzrlib.inventory import Inventory
336
from bzrlib.xml import unpack_xml
337
from time import time
323
# ElementTree does its own conversion from UTF-8, so open in
341
# ElementTree does its own conversion from UTF-8, so open in
343
inv = unpack_xml(Inventory,
344
self.controlfile('inventory', 'rb'))
327
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
345
328
mutter("loaded inventory of %d items in %f"
346
% (len(inv), time() - before))
329
% (len(inv), time.time() - before))
355
338
That is to say, the inventory describing changes underway, that
356
339
will be committed to the next revision.
358
from bzrlib.atomicfile import AtomicFile
359
from bzrlib.xml import pack_xml
363
f = AtomicFile(self.controlfilename('inventory'), 'wb')
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)
372
351
mutter('wrote working inventory')
402
381
add all non-ignored children. Perhaps do that in a
403
382
higher-level method.
405
from bzrlib.textui import show_status
406
384
# TODO: Re-adding a file that is removed in the working copy
407
385
# should probably put it back with the previous ID.
408
if isinstance(files, basestring):
409
assert(ids is None or isinstance(ids, basestring))
386
if isinstance(files, types.StringTypes):
387
assert(ids is None or isinstance(ids, types.StringTypes))
411
389
if ids is not None:
444
422
inv.add_path(f, kind=kind, file_id=file_id)
447
print 'added', quotefn(f)
425
show_status('A', kind, quotefn(f))
449
427
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
481
459
is the opposite of add. Removing it is consistent with most
482
460
other tools. Maybe an option.
484
from bzrlib.textui import show_status
485
462
## TODO: Normalize names
486
463
## TODO: Remove nested loops; better scalability
487
if isinstance(files, basestring):
464
if isinstance(files, types.StringTypes):
490
467
self.lock_write()
516
493
# FIXME: this doesn't need to be a branch method
517
494
def set_inventory(self, new_inventory_list):
518
from bzrlib.inventory import Inventory, InventoryEntry
519
495
inv = Inventory()
520
496
for path, file_id, parent, kind in new_inventory_list:
521
497
name = os.path.basename(path)
547
523
def append_revision(self, revision_id):
548
from bzrlib.atomicfile import AtomicFile
550
524
mutter("add {%s} to revision-history" % revision_id)
551
rev_history = self.revision_history() + [revision_id]
553
f = AtomicFile(self.controlfilename('revision-history'))
555
for rev_id in rev_history:
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)
562
542
def get_revision(self, revision_id):
563
543
"""Return the Revision object for a named revision"""
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])
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])
575
547
assert r.revision_id == revision_id
579
550
def get_revision_sha1(self, revision_id):
580
551
"""Hash the stored value of a revision, and return it."""
593
564
TODO: Perhaps for this and similar methods, take a revision
594
565
parameter which can be either an integer revno or a
596
from bzrlib.inventory import Inventory
597
from bzrlib.xml import unpack_xml
599
return unpack_xml(Inventory, self.inventory_store[inventory_id])
567
i = Inventory.read_xml(self.inventory_store[inventory_id])
602
570
def get_inventory_sha1(self, inventory_id):
603
571
"""Return the sha1 hash of the inventory entry
608
576
def get_revision_inventory(self, revision_id):
609
577
"""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.
612
578
if revision_id == None:
613
from bzrlib.inventory import Inventory
614
579
return Inventory()
616
return self.get_inventory(revision_id)
581
return self.get_inventory(self.get_revision(revision_id).inventory_id)
619
584
def revision_history(self):
785
750
from bzrlib.progress import ProgressBar
789
from sets import Set as set
791
752
pb = ProgressBar()
793
754
pb.update('comparing histories')
794
755
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()
806
759
for rev_id in revision_ids:
854
808
`revision_id` may be None for the null revision, in which case
855
809
an `EmptyTree` is returned."""
856
from bzrlib.tree import EmptyTree, RevisionTree
857
810
# TODO: refactor this to use an existing revision object
858
811
# so we don't need to read it in twice.
859
812
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'))
1088
954
class ScratchBranch(Branch):
1089
955
"""Special test class: a branch that cleans up after itself.
1104
970
If any files are listed, they are created in the working copy.
1106
from tempfile import mkdtemp
1108
973
if base is None:
974
base = tempfile.mkdtemp()
1111
976
Branch.__init__(self, base, init=init)
1125
990
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1128
from shutil import copytree
1129
from tempfile import mkdtemp
993
base = tempfile.mkdtemp()
1132
copytree(self.base, base, symlinks=True)
995
shutil.copytree(self.base, base, symlinks=True)
1133
996
return ScratchBranch(base=base)
1135
998
def __del__(self):
1138
1001
def destroy(self):
1139
1002
"""Destroy the test branch, removing the scratch directory."""
1140
from shutil import rmtree
1143
1005
mutter("delete ScratchBranch %s" % self.base)
1006
shutil.rmtree(self.base)
1145
1007
except OSError, e:
1146
1008
# Work around for shutil.rmtree failing on Windows when
1147
1009
# readonly files are encountered
1199
1059
name = re.sub(r'[^\w.]', '', name)
1201
1061
s = hexlify(rand_bytes(8))
1202
return '-'.join((name, compact_date(time()), s))
1062
return '-'.join((name, compact_date(time.time()), s))