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
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
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
## TODO: Maybe include checks for common corruption of newlines, etc?
40
def find_branch(f, **args):
41
if f and (f.startswith('http://') or f.startswith('https://')):
43
return remotebranch.RemoteBranch(f, **args)
45
return Branch(f, **args)
49
def _relpath(base, path):
50
"""Return path relative to base, or raise exception.
52
The path may be either an absolute path or a path relative to the
53
current working directory.
55
Lifted out of Branch.relpath for ease of testing.
57
os.path.commonprefix (python2.4) has a bad bug that it works just
58
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
59
avoids that problem."""
60
rp = os.path.abspath(path)
64
while len(head) >= len(base):
67
head, tail = os.path.split(head)
71
from errors import NotBranchError
72
raise NotBranchError("path %r is not within branch %r" % (rp, base))
77
def find_branch_root(f=None):
78
"""Find the branch root enclosing f, or pwd.
80
f may be a filename or a URL.
82
It is not necessary that f exists.
84
Basically we keep looking up until we find the control directory or
88
elif hasattr(os.path, 'realpath'):
89
f = os.path.realpath(f)
91
f = os.path.abspath(f)
92
if not os.path.exists(f):
93
raise BzrError('%r does not exist' % f)
99
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
101
head, tail = os.path.split(f)
103
# reached the root, whatever that may be
104
raise BzrError('%r is not in a branch' % orig_f)
107
class DivergedBranches(Exception):
108
def __init__(self, branch1, branch2):
109
self.branch1 = branch1
110
self.branch2 = branch2
111
Exception.__init__(self, "These branches have diverged.")
114
class NoSuchRevision(BzrError):
115
def __init__(self, branch, revision):
117
self.revision = revision
118
msg = "Branch %s has no revision %d" % (branch, revision)
119
BzrError.__init__(self, msg)
122
######################################################################
125
class Branch(object):
126
"""Branch holding a history of revisions.
129
Base directory of the branch.
135
If _lock_mode is true, a positive count of the number of times the
139
Lock object from bzrlib.lock.
146
def __init__(self, base, init=False, find_root=True):
147
"""Create new branch object at a particular location.
149
base -- Base directory for the branch.
151
init -- If True, create new control files in a previously
152
unversioned directory. If False, the branch must already
155
find_root -- If true and init is false, find the root of the
156
existing branch containing base.
158
In the test suite, creation of new trees is tested using the
159
`ScratchBranch` class.
162
self.base = os.path.realpath(base)
165
self.base = find_branch_root(base)
167
self.base = os.path.realpath(base)
168
if not isdir(self.controlfilename('.')):
169
from errors import NotBranchError
170
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
171
['use "bzr init" to initialize a new working tree',
172
'current bzr can only operate from top-of-tree'])
175
self.text_store = ImmutableStore(self.controlfilename('text-store'))
176
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
177
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
181
return '%s(%r)' % (self.__class__.__name__, self.base)
188
if self._lock_mode or self._lock:
189
from warnings import warn
190
warn("branch %r was not explicitly unlocked" % self)
195
def lock_write(self):
197
if self._lock_mode != 'w':
198
from errors import LockError
199
raise LockError("can't upgrade to a write lock from %r" %
201
self._lock_count += 1
203
from bzrlib.lock import WriteLock
205
self._lock = WriteLock(self.controlfilename('branch-lock'))
206
self._lock_mode = 'w'
213
assert self._lock_mode in ('r', 'w'), \
214
"invalid lock mode %r" % self._lock_mode
215
self._lock_count += 1
217
from bzrlib.lock import ReadLock
219
self._lock = ReadLock(self.controlfilename('branch-lock'))
220
self._lock_mode = 'r'
226
if not self._lock_mode:
227
from errors import LockError
228
raise LockError('branch %r is not locked' % (self))
230
if self._lock_count > 1:
231
self._lock_count -= 1
235
self._lock_mode = self._lock_count = None
238
def abspath(self, name):
239
"""Return absolute filename for something in the branch"""
240
return os.path.join(self.base, name)
243
def relpath(self, path):
244
"""Return path relative to this branch of something inside it.
246
Raises an error if path is not in this branch."""
247
return _relpath(self.base, path)
250
def controlfilename(self, file_or_path):
251
"""Return location relative to branch."""
252
if isinstance(file_or_path, types.StringTypes):
253
file_or_path = [file_or_path]
254
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
257
def controlfile(self, file_or_path, mode='r'):
258
"""Open a control file for this branch.
260
There are two classes of file in the control directory: text
261
and binary. binary files are untranslated byte streams. Text
262
control files are stored with Unix newlines and in UTF-8, even
263
if the platform or locale defaults are different.
265
Controlfiles should almost never be opened in write mode but
266
rather should be atomically copied and replaced using atomicfile.
269
fn = self.controlfilename(file_or_path)
271
if mode == 'rb' or mode == 'wb':
272
return file(fn, mode)
273
elif mode == 'r' or mode == 'w':
274
# open in binary mode anyhow so there's no newline translation;
275
# codecs uses line buffering by default; don't want that.
277
return codecs.open(fn, mode + 'b', 'utf-8',
280
raise BzrError("invalid controlfile mode %r" % mode)
284
def _make_control(self):
285
os.mkdir(self.controlfilename([]))
286
self.controlfile('README', 'w').write(
287
"This is a Bazaar-NG control directory.\n"
288
"Do not change any files in this directory.\n")
289
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
290
for d in ('text-store', 'inventory-store', 'revision-store'):
291
os.mkdir(self.controlfilename(d))
292
for f in ('revision-history', 'merged-patches',
293
'pending-merged-patches', 'branch-name',
295
self.controlfile(f, 'w').write('')
296
mutter('created control directory in ' + self.base)
297
Inventory().write_xml(self.controlfile('inventory','w'))
300
def _check_format(self):
301
"""Check this branch format is supported.
303
The current tool only supports the current unstable format.
305
In the future, we might need different in-memory Branch
306
classes to support downlevel branches. But not yet.
308
# This ignores newlines so that we can open branches created
309
# on Windows from Linux and so on. I think it might be better
310
# to always make all internal files in unix format.
311
fmt = self.controlfile('branch-format', 'r').read()
312
fmt.replace('\r\n', '')
313
if fmt != BZR_BRANCH_FORMAT:
314
raise BzrError('sorry, branch format %r not supported' % fmt,
315
['use a different bzr version',
316
'or remove the .bzr directory and "bzr init" again'])
320
def read_working_inventory(self):
321
"""Read the working inventory."""
323
# ElementTree does its own conversion from UTF-8, so open in
327
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
328
mutter("loaded inventory of %d items in %f"
329
% (len(inv), time.time() - before))
335
def _write_inventory(self, inv):
336
"""Update the working inventory.
338
That is to say, the inventory describing changes underway, that
339
will be committed to the next revision.
343
from bzrlib.atomicfile import AtomicFile
345
f = AtomicFile(self.controlfilename('inventory'), 'wb')
354
mutter('wrote working inventory')
357
inventory = property(read_working_inventory, _write_inventory, None,
358
"""Inventory for the working copy.""")
361
def add(self, files, verbose=False, ids=None):
362
"""Make files versioned.
364
Note that the command line normally calls smart_add instead.
366
This puts the files in the Added state, so that they will be
367
recorded by the next commit.
370
List of paths to add, relative to the base of the tree.
373
If set, use these instead of automatically generated ids.
374
Must be the same length as the list of files, but may
375
contain None for ids that are to be autogenerated.
377
TODO: Perhaps have an option to add the ids even if the files do
380
TODO: Perhaps return the ids of the files? But then again it
381
is easy to retrieve them if they're needed.
383
TODO: Adding a directory should optionally recurse down and
384
add all non-ignored children. Perhaps do that in a
387
# TODO: Re-adding a file that is removed in the working copy
388
# should probably put it back with the previous ID.
389
if isinstance(files, types.StringTypes):
390
assert(ids is None or isinstance(ids, types.StringTypes))
396
ids = [None] * len(files)
398
assert(len(ids) == len(files))
402
inv = self.read_working_inventory()
403
for f,file_id in zip(files, ids):
404
if is_control_file(f):
405
raise BzrError("cannot add control file %s" % quotefn(f))
410
raise BzrError("cannot add top-level %r" % f)
412
fullpath = os.path.normpath(self.abspath(f))
415
kind = file_kind(fullpath)
417
# maybe something better?
418
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
420
if kind != 'file' and kind != 'directory':
421
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
424
file_id = gen_file_id(f)
425
inv.add_path(f, kind=kind, file_id=file_id)
428
print 'added', quotefn(f)
430
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
432
self._write_inventory(inv)
437
def print_file(self, file, revno):
438
"""Print `file` to stdout."""
441
tree = self.revision_tree(self.lookup_revision(revno))
442
# use inventory as it was in that revision
443
file_id = tree.inventory.path2id(file)
445
raise BzrError("%r is not present in revision %d" % (file, revno))
446
tree.print_file(file_id)
451
def remove(self, files, verbose=False):
452
"""Mark nominated files for removal from the inventory.
454
This does not remove their text. This does not run on
456
TODO: Refuse to remove modified files unless --force is given?
458
TODO: Do something useful with directories.
460
TODO: Should this remove the text or not? Tough call; not
461
removing may be useful and the user can just use use rm, and
462
is the opposite of add. Removing it is consistent with most
463
other tools. Maybe an option.
465
## TODO: Normalize names
466
## TODO: Remove nested loops; better scalability
467
if isinstance(files, types.StringTypes):
473
tree = self.working_tree()
476
# do this before any modifications
480
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
481
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
483
# having remove it, it must be either ignored or unknown
484
if tree.is_ignored(f):
488
show_status(new_status, inv[fid].kind, quotefn(f))
491
self._write_inventory(inv)
496
# FIXME: this doesn't need to be a branch method
497
def set_inventory(self, new_inventory_list):
499
for path, file_id, parent, kind in new_inventory_list:
500
name = os.path.basename(path)
503
inv.add(InventoryEntry(file_id, name, kind, parent))
504
self._write_inventory(inv)
508
"""Return all unknown files.
510
These are files in the working directory that are not versioned or
511
control files or ignored.
513
>>> b = ScratchBranch(files=['foo', 'foo~'])
514
>>> list(b.unknowns())
517
>>> list(b.unknowns())
520
>>> list(b.unknowns())
523
return self.working_tree().unknowns()
526
def append_revision(self, revision_id):
527
from bzrlib.atomicfile import AtomicFile
529
mutter("add {%s} to revision-history" % revision_id)
530
rev_history = self.revision_history() + [revision_id]
532
f = AtomicFile(self.controlfilename('revision-history'))
534
for rev_id in rev_history:
541
def get_revision(self, revision_id):
542
"""Return the Revision object for a named revision"""
543
if not revision_id or not isinstance(revision_id, basestring):
544
raise ValueError('invalid revision-id: %r' % revision_id)
545
r = Revision.read_xml(self.revision_store[revision_id])
546
assert r.revision_id == revision_id
549
def get_revision_sha1(self, revision_id):
550
"""Hash the stored value of a revision, and return it."""
551
# In the future, revision entries will be signed. At that
552
# point, it is probably best *not* to include the signature
553
# in the revision hash. Because that lets you re-sign
554
# the revision, (add signatures/remove signatures) and still
555
# have all hash pointers stay consistent.
556
# But for now, just hash the contents.
557
return sha_file(self.revision_store[revision_id])
560
def get_inventory(self, inventory_id):
561
"""Get Inventory object by hash.
563
TODO: Perhaps for this and similar methods, take a revision
564
parameter which can be either an integer revno or a
566
i = Inventory.read_xml(self.inventory_store[inventory_id])
569
def get_inventory_sha1(self, inventory_id):
570
"""Return the sha1 hash of the inventory entry
572
return sha_file(self.inventory_store[inventory_id])
575
def get_revision_inventory(self, revision_id):
576
"""Return inventory of a past revision."""
577
if revision_id == None:
580
return self.get_inventory(self.get_revision(revision_id).inventory_id)
583
def revision_history(self):
584
"""Return sequence of revision hashes on to this branch.
586
>>> ScratchBranch().revision_history()
591
return [l.rstrip('\r\n') for l in
592
self.controlfile('revision-history', 'r').readlines()]
597
def common_ancestor(self, other, self_revno=None, other_revno=None):
600
>>> sb = ScratchBranch(files=['foo', 'foo~'])
601
>>> sb.common_ancestor(sb) == (None, None)
603
>>> commit.commit(sb, "Committing first revision", verbose=False)
604
>>> sb.common_ancestor(sb)[0]
606
>>> clone = sb.clone()
607
>>> commit.commit(sb, "Committing second revision", verbose=False)
608
>>> sb.common_ancestor(sb)[0]
610
>>> sb.common_ancestor(clone)[0]
612
>>> commit.commit(clone, "Committing divergent second revision",
614
>>> sb.common_ancestor(clone)[0]
616
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
618
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
620
>>> clone2 = sb.clone()
621
>>> sb.common_ancestor(clone2)[0]
623
>>> sb.common_ancestor(clone2, self_revno=1)[0]
625
>>> sb.common_ancestor(clone2, other_revno=1)[0]
628
my_history = self.revision_history()
629
other_history = other.revision_history()
630
if self_revno is None:
631
self_revno = len(my_history)
632
if other_revno is None:
633
other_revno = len(other_history)
634
indices = range(min((self_revno, other_revno)))
637
if my_history[r] == other_history[r]:
638
return r+1, my_history[r]
641
def enum_history(self, direction):
642
"""Return (revno, revision_id) for history of branch.
645
'forward' is from earliest to latest
646
'reverse' is from latest to earliest
648
rh = self.revision_history()
649
if direction == 'forward':
654
elif direction == 'reverse':
660
raise ValueError('invalid history direction', direction)
664
"""Return current revision number for this branch.
666
That is equivalent to the number of revisions committed to
669
return len(self.revision_history())
672
def last_patch(self):
673
"""Return last patch hash, or None if no history.
675
ph = self.revision_history()
682
def missing_revisions(self, other, stop_revision=None):
684
If self and other have not diverged, return a list of the revisions
685
present in other, but missing from self.
687
>>> from bzrlib.commit import commit
688
>>> bzrlib.trace.silent = True
689
>>> br1 = ScratchBranch()
690
>>> br2 = ScratchBranch()
691
>>> br1.missing_revisions(br2)
693
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
694
>>> br1.missing_revisions(br2)
696
>>> br2.missing_revisions(br1)
698
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
699
>>> br1.missing_revisions(br2)
701
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
702
>>> br1.missing_revisions(br2)
704
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
705
>>> br1.missing_revisions(br2)
706
Traceback (most recent call last):
707
DivergedBranches: These branches have diverged.
709
self_history = self.revision_history()
710
self_len = len(self_history)
711
other_history = other.revision_history()
712
other_len = len(other_history)
713
common_index = min(self_len, other_len) -1
714
if common_index >= 0 and \
715
self_history[common_index] != other_history[common_index]:
716
raise DivergedBranches(self, other)
718
if stop_revision is None:
719
stop_revision = other_len
720
elif stop_revision > other_len:
721
raise NoSuchRevision(self, stop_revision)
723
return other_history[self_len:stop_revision]
726
def update_revisions(self, other, stop_revision=None):
727
"""Pull in all new revisions from other branch.
729
>>> from bzrlib.commit import commit
730
>>> bzrlib.trace.silent = True
731
>>> br1 = ScratchBranch(files=['foo', 'bar'])
734
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
735
>>> br2 = ScratchBranch()
736
>>> br2.update_revisions(br1)
740
>>> br2.revision_history()
742
>>> br2.update_revisions(br1)
746
>>> br1.text_store.total_size() == br2.text_store.total_size()
749
from bzrlib.progress import ProgressBar
753
pb.update('comparing histories')
754
revision_ids = self.missing_revisions(other, stop_revision)
756
needed_texts = sets.Set()
758
for rev_id in revision_ids:
760
pb.update('fetching revision', i, len(revision_ids))
761
rev = other.get_revision(rev_id)
762
revisions.append(rev)
763
inv = other.get_inventory(str(rev.inventory_id))
764
for key, entry in inv.iter_entries():
765
if entry.text_id is None:
767
if entry.text_id not in self.text_store:
768
needed_texts.add(entry.text_id)
772
count = self.text_store.copy_multi(other.text_store, needed_texts)
773
print "Added %d texts." % count
774
inventory_ids = [ f.inventory_id for f in revisions ]
775
count = self.inventory_store.copy_multi(other.inventory_store,
777
print "Added %d inventories." % count
778
revision_ids = [ f.revision_id for f in revisions]
779
count = self.revision_store.copy_multi(other.revision_store,
781
for revision_id in revision_ids:
782
self.append_revision(revision_id)
783
print "Added %d revisions." % count
786
def commit(self, *args, **kw):
787
from bzrlib.commit import commit
788
commit(self, *args, **kw)
791
def lookup_revision(self, revno):
792
"""Return revision hash for revision number."""
797
# list is 0-based; revisions are 1-based
798
return self.revision_history()[revno-1]
800
raise BzrError("no such revision %s" % revno)
803
def revision_tree(self, revision_id):
804
"""Return Tree for a revision on this branch.
806
`revision_id` may be None for the null revision, in which case
807
an `EmptyTree` is returned."""
808
# TODO: refactor this to use an existing revision object
809
# so we don't need to read it in twice.
810
if revision_id == None:
813
inv = self.get_revision_inventory(revision_id)
814
return RevisionTree(self.text_store, inv)
817
def working_tree(self):
818
"""Return a `Tree` for the working copy."""
819
from workingtree import WorkingTree
820
return WorkingTree(self.base, self.read_working_inventory())
823
def basis_tree(self):
824
"""Return `Tree` object for last revision.
826
If there are no revisions yet, return an `EmptyTree`.
828
r = self.last_patch()
832
return RevisionTree(self.text_store, self.get_revision_inventory(r))
836
def rename_one(self, from_rel, to_rel):
839
This can change the directory or the filename or both.
843
tree = self.working_tree()
845
if not tree.has_filename(from_rel):
846
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
847
if tree.has_filename(to_rel):
848
raise BzrError("can't rename: new working file %r already exists" % to_rel)
850
file_id = inv.path2id(from_rel)
852
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
854
if inv.path2id(to_rel):
855
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
857
to_dir, to_tail = os.path.split(to_rel)
858
to_dir_id = inv.path2id(to_dir)
859
if to_dir_id == None and to_dir != '':
860
raise BzrError("can't determine destination directory id for %r" % to_dir)
862
mutter("rename_one:")
863
mutter(" file_id {%s}" % file_id)
864
mutter(" from_rel %r" % from_rel)
865
mutter(" to_rel %r" % to_rel)
866
mutter(" to_dir %r" % to_dir)
867
mutter(" to_dir_id {%s}" % to_dir_id)
869
inv.rename(file_id, to_dir_id, to_tail)
871
print "%s => %s" % (from_rel, to_rel)
873
from_abs = self.abspath(from_rel)
874
to_abs = self.abspath(to_rel)
876
os.rename(from_abs, to_abs)
878
raise BzrError("failed to rename %r to %r: %s"
879
% (from_abs, to_abs, e[1]),
880
["rename rolled back"])
882
self._write_inventory(inv)
887
def move(self, from_paths, to_name):
890
to_name must exist as a versioned directory.
892
If to_name exists and is a directory, the files are moved into
893
it, keeping their old names. If it is a directory,
895
Note that to_name is only the last component of the new name;
896
this doesn't change the directory.
900
## TODO: Option to move IDs only
901
assert not isinstance(from_paths, basestring)
902
tree = self.working_tree()
904
to_abs = self.abspath(to_name)
905
if not isdir(to_abs):
906
raise BzrError("destination %r is not a directory" % to_abs)
907
if not tree.has_filename(to_name):
908
raise BzrError("destination %r not in working directory" % to_abs)
909
to_dir_id = inv.path2id(to_name)
910
if to_dir_id == None and to_name != '':
911
raise BzrError("destination %r is not a versioned directory" % to_name)
912
to_dir_ie = inv[to_dir_id]
913
if to_dir_ie.kind not in ('directory', 'root_directory'):
914
raise BzrError("destination %r is not a directory" % to_abs)
916
to_idpath = inv.get_idpath(to_dir_id)
919
if not tree.has_filename(f):
920
raise BzrError("%r does not exist in working tree" % f)
921
f_id = inv.path2id(f)
923
raise BzrError("%r is not versioned" % f)
924
name_tail = splitpath(f)[-1]
925
dest_path = appendpath(to_name, name_tail)
926
if tree.has_filename(dest_path):
927
raise BzrError("destination %r already exists" % dest_path)
928
if f_id in to_idpath:
929
raise BzrError("can't move %r to a subdirectory of itself" % f)
931
# OK, so there's a race here, it's possible that someone will
932
# create a file in this interval and then the rename might be
933
# left half-done. But we should have caught most problems.
936
name_tail = splitpath(f)[-1]
937
dest_path = appendpath(to_name, name_tail)
938
print "%s => %s" % (f, dest_path)
939
inv.rename(inv.path2id(f), to_dir_id, name_tail)
941
os.rename(self.abspath(f), self.abspath(dest_path))
943
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
944
["rename rolled back"])
946
self._write_inventory(inv)
951
def revert(self, filenames, old_tree=None, backups=True):
952
"""Restore selected files to the versions from a previous tree.
955
If true (default) backups are made of files before
958
from bzrlib.errors import NotVersionedError, BzrError
959
from bzrlib.atomicfile import AtomicFile
960
from bzrlib.osutils import backup_file
962
inv = self.read_working_inventory()
964
old_tree = self.basis_tree()
965
old_inv = old_tree.inventory
969
file_id = inv.path2id(fn)
971
raise NotVersionedError("not a versioned file", fn)
972
if not old_inv.has_id(file_id):
973
raise BzrError("file not present in old tree", fn, file_id)
974
nids.append((fn, file_id))
976
# TODO: Rename back if it was previously at a different location
978
# TODO: If given a directory, restore the entire contents from
979
# the previous version.
981
# TODO: Make a backup to a temporary file.
983
# TODO: If the file previously didn't exist, delete it?
984
for fn, file_id in nids:
987
f = AtomicFile(fn, 'wb')
989
f.write(old_tree.get_file(file_id).read())
996
class ScratchBranch(Branch):
997
"""Special test class: a branch that cleans up after itself.
999
>>> b = ScratchBranch()
1007
def __init__(self, files=[], dirs=[], base=None):
1008
"""Make a test branch.
1010
This creates a temporary directory and runs init-tree in it.
1012
If any files are listed, they are created in the working copy.
1016
base = tempfile.mkdtemp()
1018
Branch.__init__(self, base, init=init)
1020
os.mkdir(self.abspath(d))
1023
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1028
>>> orig = ScratchBranch(files=["file1", "file2"])
1029
>>> clone = orig.clone()
1030
>>> os.path.samefile(orig.base, clone.base)
1032
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1035
base = tempfile.mkdtemp()
1037
shutil.copytree(self.base, base, symlinks=True)
1038
return ScratchBranch(base=base)
1044
"""Destroy the test branch, removing the scratch directory."""
1047
mutter("delete ScratchBranch %s" % self.base)
1048
shutil.rmtree(self.base)
1050
# Work around for shutil.rmtree failing on Windows when
1051
# readonly files are encountered
1052
mutter("hit exception in destroying ScratchBranch: %s" % e)
1053
for root, dirs, files in os.walk(self.base, topdown=False):
1055
os.chmod(os.path.join(root, name), 0700)
1056
shutil.rmtree(self.base)
1061
######################################################################
1065
def is_control_file(filename):
1066
## FIXME: better check
1067
filename = os.path.normpath(filename)
1068
while filename != '':
1069
head, tail = os.path.split(filename)
1070
## mutter('check %r for control file' % ((head, tail), ))
1071
if tail == bzrlib.BZRDIR:
1073
if filename == head:
1080
def gen_file_id(name):
1081
"""Return new file id.
1083
This should probably generate proper UUIDs, but for the moment we
1084
cope with just randomness because running uuidgen every time is
1088
# get last component
1089
idx = name.rfind('/')
1091
name = name[idx+1 : ]
1092
idx = name.rfind('\\')
1094
name = name[idx+1 : ]
1096
# make it not a hidden file
1097
name = name.lstrip('.')
1099
# remove any wierd characters; we don't escape them but rather
1100
# just pull them out
1101
name = re.sub(r'[^\w.]', '', name)
1103
s = hexlify(rand_bytes(8))
1104
return '-'.join((name, compact_date(time.time()), s))