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.")
113
######################################################################
116
class Branch(object):
117
"""Branch holding a history of revisions.
120
Base directory of the branch.
126
If _lock_mode is true, a positive count of the number of times the
130
Lock object from bzrlib.lock.
137
def __init__(self, base, init=False, find_root=True):
138
"""Create new branch object at a particular location.
140
base -- Base directory for the branch.
142
init -- If True, create new control files in a previously
143
unversioned directory. If False, the branch must already
146
find_root -- If true and init is false, find the root of the
147
existing branch containing base.
149
In the test suite, creation of new trees is tested using the
150
`ScratchBranch` class.
153
self.base = os.path.realpath(base)
156
self.base = find_branch_root(base)
158
self.base = os.path.realpath(base)
159
if not isdir(self.controlfilename('.')):
160
from errors import NotBranchError
161
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
162
['use "bzr init" to initialize a new working tree',
163
'current bzr can only operate from top-of-tree'])
166
self.text_store = ImmutableStore(self.controlfilename('text-store'))
167
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
168
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
172
return '%s(%r)' % (self.__class__.__name__, self.base)
179
if self._lock_mode or self._lock:
180
from warnings import warn
181
warn("branch %r was not explicitly unlocked" % self)
186
def lock_write(self):
188
if self._lock_mode != 'w':
189
from errors import LockError
190
raise LockError("can't upgrade to a write lock from %r" %
192
self._lock_count += 1
194
from bzrlib.lock import WriteLock
196
self._lock = WriteLock(self.controlfilename('branch-lock'))
197
self._lock_mode = 'w'
204
assert self._lock_mode in ('r', 'w'), \
205
"invalid lock mode %r" % self._lock_mode
206
self._lock_count += 1
208
from bzrlib.lock import ReadLock
210
self._lock = ReadLock(self.controlfilename('branch-lock'))
211
self._lock_mode = 'r'
217
if not self._lock_mode:
218
from errors import LockError
219
raise LockError('branch %r is not locked' % (self))
221
if self._lock_count > 1:
222
self._lock_count -= 1
226
self._lock_mode = self._lock_count = None
229
def abspath(self, name):
230
"""Return absolute filename for something in the branch"""
231
return os.path.join(self.base, name)
234
def relpath(self, path):
235
"""Return path relative to this branch of something inside it.
237
Raises an error if path is not in this branch."""
238
return _relpath(self.base, path)
241
def controlfilename(self, file_or_path):
242
"""Return location relative to branch."""
243
if isinstance(file_or_path, types.StringTypes):
244
file_or_path = [file_or_path]
245
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
248
def controlfile(self, file_or_path, mode='r'):
249
"""Open a control file for this branch.
251
There are two classes of file in the control directory: text
252
and binary. binary files are untranslated byte streams. Text
253
control files are stored with Unix newlines and in UTF-8, even
254
if the platform or locale defaults are different.
256
Controlfiles should almost never be opened in write mode but
257
rather should be atomically copied and replaced using atomicfile.
260
fn = self.controlfilename(file_or_path)
262
if mode == 'rb' or mode == 'wb':
263
return file(fn, mode)
264
elif mode == 'r' or mode == 'w':
265
# open in binary mode anyhow so there's no newline translation;
266
# codecs uses line buffering by default; don't want that.
268
return codecs.open(fn, mode + 'b', 'utf-8',
271
raise BzrError("invalid controlfile mode %r" % mode)
275
def _make_control(self):
276
os.mkdir(self.controlfilename([]))
277
self.controlfile('README', 'w').write(
278
"This is a Bazaar-NG control directory.\n"
279
"Do not change any files in this directory.")
280
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
281
for d in ('text-store', 'inventory-store', 'revision-store'):
282
os.mkdir(self.controlfilename(d))
283
for f in ('revision-history', 'merged-patches',
284
'pending-merged-patches', 'branch-name',
286
self.controlfile(f, 'w').write('')
287
mutter('created control directory in ' + self.base)
288
Inventory().write_xml(self.controlfile('inventory','w'))
291
def _check_format(self):
292
"""Check this branch format is supported.
294
The current tool only supports the current unstable format.
296
In the future, we might need different in-memory Branch
297
classes to support downlevel branches. But not yet.
299
# This ignores newlines so that we can open branches created
300
# on Windows from Linux and so on. I think it might be better
301
# to always make all internal files in unix format.
302
fmt = self.controlfile('branch-format', 'r').read()
303
fmt.replace('\r\n', '')
304
if fmt != BZR_BRANCH_FORMAT:
305
raise BzrError('sorry, branch format %r not supported' % fmt,
306
['use a different bzr version',
307
'or remove the .bzr directory and "bzr init" again'])
311
def read_working_inventory(self):
312
"""Read the working inventory."""
314
# ElementTree does its own conversion from UTF-8, so open in
318
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
319
mutter("loaded inventory of %d items in %f"
320
% (len(inv), time.time() - before))
326
def _write_inventory(self, inv):
327
"""Update the working inventory.
329
That is to say, the inventory describing changes underway, that
330
will be committed to the next revision.
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)
342
mutter('wrote working inventory')
345
inventory = property(read_working_inventory, _write_inventory, None,
346
"""Inventory for the working copy.""")
349
def add(self, files, verbose=False, ids=None):
350
"""Make files versioned.
352
Note that the command line normally calls smart_add instead.
354
This puts the files in the Added state, so that they will be
355
recorded by the next commit.
358
List of paths to add, relative to the base of the tree.
361
If set, use these instead of automatically generated ids.
362
Must be the same length as the list of files, but may
363
contain None for ids that are to be autogenerated.
365
TODO: Perhaps have an option to add the ids even if the files do
368
TODO: Perhaps return the ids of the files? But then again it
369
is easy to retrieve them if they're needed.
371
TODO: Adding a directory should optionally recurse down and
372
add all non-ignored children. Perhaps do that in a
375
# TODO: Re-adding a file that is removed in the working copy
376
# should probably put it back with the previous ID.
377
if isinstance(files, types.StringTypes):
378
assert(ids is None or isinstance(ids, types.StringTypes))
384
ids = [None] * len(files)
386
assert(len(ids) == len(files))
390
inv = self.read_working_inventory()
391
for f,file_id in zip(files, ids):
392
if is_control_file(f):
393
raise BzrError("cannot add control file %s" % quotefn(f))
398
raise BzrError("cannot add top-level %r" % f)
400
fullpath = os.path.normpath(self.abspath(f))
403
kind = file_kind(fullpath)
405
# maybe something better?
406
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
408
if kind != 'file' and kind != 'directory':
409
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
412
file_id = gen_file_id(f)
413
inv.add_path(f, kind=kind, file_id=file_id)
416
show_status('A', kind, quotefn(f))
418
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
420
self._write_inventory(inv)
425
def print_file(self, file, revno):
426
"""Print `file` to stdout."""
429
tree = self.revision_tree(self.lookup_revision(revno))
430
# use inventory as it was in that revision
431
file_id = tree.inventory.path2id(file)
433
raise BzrError("%r is not present in revision %d" % (file, revno))
434
tree.print_file(file_id)
439
def remove(self, files, verbose=False):
440
"""Mark nominated files for removal from the inventory.
442
This does not remove their text. This does not run on
444
TODO: Refuse to remove modified files unless --force is given?
446
TODO: Do something useful with directories.
448
TODO: Should this remove the text or not? Tough call; not
449
removing may be useful and the user can just use use rm, and
450
is the opposite of add. Removing it is consistent with most
451
other tools. Maybe an option.
453
## TODO: Normalize names
454
## TODO: Remove nested loops; better scalability
455
if isinstance(files, types.StringTypes):
461
tree = self.working_tree()
464
# do this before any modifications
468
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
469
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
471
# having remove it, it must be either ignored or unknown
472
if tree.is_ignored(f):
476
show_status(new_status, inv[fid].kind, quotefn(f))
479
self._write_inventory(inv)
484
# FIXME: this doesn't need to be a branch method
485
def set_inventory(self, new_inventory_list):
487
for path, file_id, parent, kind in new_inventory_list:
488
name = os.path.basename(path)
491
inv.add(InventoryEntry(file_id, name, kind, parent))
492
self._write_inventory(inv)
496
"""Return all unknown files.
498
These are files in the working directory that are not versioned or
499
control files or ignored.
501
>>> b = ScratchBranch(files=['foo', 'foo~'])
502
>>> list(b.unknowns())
505
>>> list(b.unknowns())
508
>>> list(b.unknowns())
511
return self.working_tree().unknowns()
514
def append_revision(self, revision_id):
515
mutter("add {%s} to revision-history" % revision_id)
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)
533
def get_revision(self, revision_id):
534
"""Return the Revision object for a named revision"""
535
if not revision_id or not isinstance(revision_id, basestring):
536
raise ValueError('invalid revision-id: %r' % revision_id)
537
r = Revision.read_xml(self.revision_store[revision_id])
538
assert r.revision_id == revision_id
541
def get_revision_sha1(self, revision_id):
542
"""Hash the stored value of a revision, and return it."""
543
# In the future, revision entries will be signed. At that
544
# point, it is probably best *not* to include the signature
545
# in the revision hash. Because that lets you re-sign
546
# the revision, (add signatures/remove signatures) and still
547
# have all hash pointers stay consistent.
548
# But for now, just hash the contents.
549
return sha_file(self.revision_store[revision_id])
552
def get_inventory(self, inventory_id):
553
"""Get Inventory object by hash.
555
TODO: Perhaps for this and similar methods, take a revision
556
parameter which can be either an integer revno or a
558
i = Inventory.read_xml(self.inventory_store[inventory_id])
561
def get_inventory_sha1(self, inventory_id):
562
"""Return the sha1 hash of the inventory entry
564
return sha_file(self.inventory_store[inventory_id])
567
def get_revision_inventory(self, revision_id):
568
"""Return inventory of a past revision."""
569
if revision_id == None:
572
return self.get_inventory(self.get_revision(revision_id).inventory_id)
575
def revision_history(self):
576
"""Return sequence of revision hashes on to this branch.
578
>>> ScratchBranch().revision_history()
583
return [l.rstrip('\r\n') for l in
584
self.controlfile('revision-history', 'r').readlines()]
589
def common_ancestor(self, other, self_revno=None, other_revno=None):
592
>>> sb = ScratchBranch(files=['foo', 'foo~'])
593
>>> sb.common_ancestor(sb) == (None, None)
595
>>> commit.commit(sb, "Committing first revision", verbose=False)
596
>>> sb.common_ancestor(sb)[0]
598
>>> clone = sb.clone()
599
>>> commit.commit(sb, "Committing second revision", verbose=False)
600
>>> sb.common_ancestor(sb)[0]
602
>>> sb.common_ancestor(clone)[0]
604
>>> commit.commit(clone, "Committing divergent second revision",
606
>>> sb.common_ancestor(clone)[0]
608
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
610
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
612
>>> clone2 = sb.clone()
613
>>> sb.common_ancestor(clone2)[0]
615
>>> sb.common_ancestor(clone2, self_revno=1)[0]
617
>>> sb.common_ancestor(clone2, other_revno=1)[0]
620
my_history = self.revision_history()
621
other_history = other.revision_history()
622
if self_revno is None:
623
self_revno = len(my_history)
624
if other_revno is None:
625
other_revno = len(other_history)
626
indices = range(min((self_revno, other_revno)))
629
if my_history[r] == other_history[r]:
630
return r+1, my_history[r]
633
def enum_history(self, direction):
634
"""Return (revno, revision_id) for history of branch.
637
'forward' is from earliest to latest
638
'reverse' is from latest to earliest
640
rh = self.revision_history()
641
if direction == 'forward':
646
elif direction == 'reverse':
652
raise ValueError('invalid history direction', direction)
656
"""Return current revision number for this branch.
658
That is equivalent to the number of revisions committed to
661
return len(self.revision_history())
664
def last_patch(self):
665
"""Return last patch hash, or None if no history.
667
ph = self.revision_history()
674
def missing_revisions(self, other):
676
If self and other have not diverged, return a list of the revisions
677
present in other, but missing from self.
679
>>> from bzrlib.commit import commit
680
>>> bzrlib.trace.silent = True
681
>>> br1 = ScratchBranch()
682
>>> br2 = ScratchBranch()
683
>>> br1.missing_revisions(br2)
685
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
686
>>> br1.missing_revisions(br2)
688
>>> br2.missing_revisions(br1)
690
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
691
>>> br1.missing_revisions(br2)
693
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
694
>>> br1.missing_revisions(br2)
696
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
697
>>> br1.missing_revisions(br2)
698
Traceback (most recent call last):
699
DivergedBranches: These branches have diverged.
701
self_history = self.revision_history()
702
self_len = len(self_history)
703
other_history = other.revision_history()
704
other_len = len(other_history)
705
common_index = min(self_len, other_len) -1
706
if common_index >= 0 and \
707
self_history[common_index] != other_history[common_index]:
708
raise DivergedBranches(self, other)
709
if self_len < other_len:
710
return other_history[self_len:]
714
def update_revisions(self, other):
715
"""Pull in all new revisions from other branch.
717
>>> from bzrlib.commit import commit
718
>>> bzrlib.trace.silent = True
719
>>> br1 = ScratchBranch(files=['foo', 'bar'])
722
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
723
>>> br2 = ScratchBranch()
724
>>> br2.update_revisions(br1)
728
>>> br2.revision_history()
730
>>> br2.update_revisions(br1)
734
>>> br1.text_store.total_size() == br2.text_store.total_size()
737
from bzrlib.progress import ProgressBar
741
pb.update('comparing histories')
742
revision_ids = self.missing_revisions(other)
744
needed_texts = sets.Set()
746
for rev_id in revision_ids:
748
pb.update('fetching revision', i, len(revision_ids))
749
rev = other.get_revision(rev_id)
750
revisions.append(rev)
751
inv = other.get_inventory(str(rev.inventory_id))
752
for key, entry in inv.iter_entries():
753
if entry.text_id is None:
755
if entry.text_id not in self.text_store:
756
needed_texts.add(entry.text_id)
760
count = self.text_store.copy_multi(other.text_store, needed_texts)
761
print "Added %d texts." % count
762
inventory_ids = [ f.inventory_id for f in revisions ]
763
count = self.inventory_store.copy_multi(other.inventory_store,
765
print "Added %d inventories." % count
766
revision_ids = [ f.revision_id for f in revisions]
767
count = self.revision_store.copy_multi(other.revision_store,
769
for revision_id in revision_ids:
770
self.append_revision(revision_id)
771
print "Added %d revisions." % count
774
def commit(self, *args, **kw):
776
from bzrlib.commit import commit
777
commit(self, *args, **kw)
780
def lookup_revision(self, revno):
781
"""Return revision hash for revision number."""
786
# list is 0-based; revisions are 1-based
787
return self.revision_history()[revno-1]
789
raise BzrError("no such revision %s" % revno)
792
def revision_tree(self, revision_id):
793
"""Return Tree for a revision on this branch.
795
`revision_id` may be None for the null revision, in which case
796
an `EmptyTree` is returned."""
797
# TODO: refactor this to use an existing revision object
798
# so we don't need to read it in twice.
799
if revision_id == None:
802
inv = self.get_revision_inventory(revision_id)
803
return RevisionTree(self.text_store, inv)
806
def working_tree(self):
807
"""Return a `Tree` for the working copy."""
808
from workingtree import WorkingTree
809
return WorkingTree(self.base, self.read_working_inventory())
812
def basis_tree(self):
813
"""Return `Tree` object for last revision.
815
If there are no revisions yet, return an `EmptyTree`.
817
r = self.last_patch()
821
return RevisionTree(self.text_store, self.get_revision_inventory(r))
825
def rename_one(self, from_rel, to_rel):
828
This can change the directory or the filename or both.
832
tree = self.working_tree()
834
if not tree.has_filename(from_rel):
835
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
836
if tree.has_filename(to_rel):
837
raise BzrError("can't rename: new working file %r already exists" % to_rel)
839
file_id = inv.path2id(from_rel)
841
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
843
if inv.path2id(to_rel):
844
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
846
to_dir, to_tail = os.path.split(to_rel)
847
to_dir_id = inv.path2id(to_dir)
848
if to_dir_id == None and to_dir != '':
849
raise BzrError("can't determine destination directory id for %r" % to_dir)
851
mutter("rename_one:")
852
mutter(" file_id {%s}" % file_id)
853
mutter(" from_rel %r" % from_rel)
854
mutter(" to_rel %r" % to_rel)
855
mutter(" to_dir %r" % to_dir)
856
mutter(" to_dir_id {%s}" % to_dir_id)
858
inv.rename(file_id, to_dir_id, to_tail)
860
print "%s => %s" % (from_rel, to_rel)
862
from_abs = self.abspath(from_rel)
863
to_abs = self.abspath(to_rel)
865
os.rename(from_abs, to_abs)
867
raise BzrError("failed to rename %r to %r: %s"
868
% (from_abs, to_abs, e[1]),
869
["rename rolled back"])
871
self._write_inventory(inv)
876
def move(self, from_paths, to_name):
879
to_name must exist as a versioned directory.
881
If to_name exists and is a directory, the files are moved into
882
it, keeping their old names. If it is a directory,
884
Note that to_name is only the last component of the new name;
885
this doesn't change the directory.
889
## TODO: Option to move IDs only
890
assert not isinstance(from_paths, basestring)
891
tree = self.working_tree()
893
to_abs = self.abspath(to_name)
894
if not isdir(to_abs):
895
raise BzrError("destination %r is not a directory" % to_abs)
896
if not tree.has_filename(to_name):
897
raise BzrError("destination %r not in working directory" % to_abs)
898
to_dir_id = inv.path2id(to_name)
899
if to_dir_id == None and to_name != '':
900
raise BzrError("destination %r is not a versioned directory" % to_name)
901
to_dir_ie = inv[to_dir_id]
902
if to_dir_ie.kind not in ('directory', 'root_directory'):
903
raise BzrError("destination %r is not a directory" % to_abs)
905
to_idpath = inv.get_idpath(to_dir_id)
908
if not tree.has_filename(f):
909
raise BzrError("%r does not exist in working tree" % f)
910
f_id = inv.path2id(f)
912
raise BzrError("%r is not versioned" % f)
913
name_tail = splitpath(f)[-1]
914
dest_path = appendpath(to_name, name_tail)
915
if tree.has_filename(dest_path):
916
raise BzrError("destination %r already exists" % dest_path)
917
if f_id in to_idpath:
918
raise BzrError("can't move %r to a subdirectory of itself" % f)
920
# OK, so there's a race here, it's possible that someone will
921
# create a file in this interval and then the rename might be
922
# left half-done. But we should have caught most problems.
925
name_tail = splitpath(f)[-1]
926
dest_path = appendpath(to_name, name_tail)
927
print "%s => %s" % (f, dest_path)
928
inv.rename(inv.path2id(f), to_dir_id, name_tail)
930
os.rename(self.abspath(f), self.abspath(dest_path))
932
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
933
["rename rolled back"])
935
self._write_inventory(inv)
941
class ScratchBranch(Branch):
942
"""Special test class: a branch that cleans up after itself.
944
>>> b = ScratchBranch()
952
def __init__(self, files=[], dirs=[], base=None):
953
"""Make a test branch.
955
This creates a temporary directory and runs init-tree in it.
957
If any files are listed, they are created in the working copy.
961
base = tempfile.mkdtemp()
963
Branch.__init__(self, base, init=init)
965
os.mkdir(self.abspath(d))
968
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
973
>>> orig = ScratchBranch(files=["file1", "file2"])
974
>>> clone = orig.clone()
975
>>> os.path.samefile(orig.base, clone.base)
977
>>> os.path.isfile(os.path.join(clone.base, "file1"))
980
base = tempfile.mkdtemp()
982
shutil.copytree(self.base, base, symlinks=True)
983
return ScratchBranch(base=base)
989
"""Destroy the test branch, removing the scratch directory."""
992
mutter("delete ScratchBranch %s" % self.base)
993
shutil.rmtree(self.base)
995
# Work around for shutil.rmtree failing on Windows when
996
# readonly files are encountered
997
mutter("hit exception in destroying ScratchBranch: %s" % e)
998
for root, dirs, files in os.walk(self.base, topdown=False):
1000
os.chmod(os.path.join(root, name), 0700)
1001
shutil.rmtree(self.base)
1006
######################################################################
1010
def is_control_file(filename):
1011
## FIXME: better check
1012
filename = os.path.normpath(filename)
1013
while filename != '':
1014
head, tail = os.path.split(filename)
1015
## mutter('check %r for control file' % ((head, tail), ))
1016
if tail == bzrlib.BZRDIR:
1018
if filename == head:
1025
def gen_file_id(name):
1026
"""Return new file id.
1028
This should probably generate proper UUIDs, but for the moment we
1029
cope with just randomness because running uuidgen every time is
1033
# get last component
1034
idx = name.rfind('/')
1036
name = name[idx+1 : ]
1037
idx = name.rfind('\\')
1039
name = name[idx+1 : ]
1041
# make it not a hidden file
1042
name = name.lstrip('.')
1044
# remove any wierd characters; we don't escape them but rather
1045
# just pull them out
1046
name = re.sub(r'[^\w.]', '', name)
1048
s = hexlify(rand_bytes(8))
1049
return '-'.join((name, compact_date(time.time()), s))