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
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
import traceback, socket, fnmatch, difflib, time
22
from binascii import hexlify
25
from inventory import Inventory
26
from trace import mutter, note
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
from inventory import InventoryEntry, Inventory
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
32
from store import ImmutableStore
33
from revision import Revision
34
from errors import bailout, BzrError
35
from textui import show_status
36
from diff import diff_trees
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
## TODO: Maybe include checks for common corruption of newlines, etc?
43
def find_branch_root(f=None):
44
"""Find the branch root enclosing f, or pwd.
46
It is not necessary that f exists.
48
Basically we keep looking up until we find the control directory or
52
elif hasattr(os.path, 'realpath'):
53
f = os.path.realpath(f)
55
f = os.path.abspath(f)
60
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
head, tail = os.path.split(f)
64
# reached the root, whatever that may be
65
raise BzrError('%r is not in a branch' % orig_f)
70
######################################################################
74
"""Branch holding a history of revisions.
77
Base directory of the branch.
81
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
82
"""Create new branch object at a particular location.
84
base -- Base directory for the branch.
86
init -- If True, create new control files in a previously
87
unversioned directory. If False, the branch must already
90
find_root -- If true and init is false, find the root of the
91
existing branch containing base.
93
In the test suite, creation of new trees is tested using the
94
`ScratchBranch` class.
97
self.base = os.path.realpath(base)
100
self.base = find_branch_root(base)
102
self.base = os.path.realpath(base)
103
if not isdir(self.controlfilename('.')):
104
bailout("not a bzr branch: %s" % quotefn(base),
105
['use "bzr init" to initialize a new working tree',
106
'current bzr can only operate from top-of-tree'])
110
self.text_store = ImmutableStore(self.controlfilename('text-store'))
111
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
112
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
116
return '%s(%r)' % (self.__class__.__name__, self.base)
123
def lock(self, mode='w'):
124
"""Lock the on-disk branch, excluding other processes."""
130
om = os.O_WRONLY | os.O_CREAT
135
raise BzrError("invalid locking mode %r" % mode)
137
lockfile = os.open(self.controlfilename('branch-lock'), om)
138
fcntl.lockf(lockfile, lm)
140
fcntl.lockf(lockfile, fcntl.LOCK_UN)
142
self._lockmode = None
144
self._lockmode = mode
146
warning("please write a locking method for platform %r" % sys.platform)
148
self._lockmode = None
150
self._lockmode = mode
153
def _need_readlock(self):
154
if self._lockmode not in ['r', 'w']:
155
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
157
def _need_writelock(self):
158
if self._lockmode not in ['w']:
159
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
162
def abspath(self, name):
163
"""Return absolute filename for something in the branch"""
164
return os.path.join(self.base, name)
167
def relpath(self, path):
168
"""Return path relative to this branch of something inside it.
170
Raises an error if path is not in this branch."""
171
rp = os.path.realpath(path)
173
if not rp.startswith(self.base):
174
bailout("path %r is not within branch %r" % (rp, self.base))
175
rp = rp[len(self.base):]
176
rp = rp.lstrip(os.sep)
180
def controlfilename(self, file_or_path):
181
"""Return location relative to branch."""
182
if isinstance(file_or_path, types.StringTypes):
183
file_or_path = [file_or_path]
184
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
187
def controlfile(self, file_or_path, mode='r'):
188
"""Open a control file for this branch.
190
There are two classes of file in the control directory: text
191
and binary. binary files are untranslated byte streams. Text
192
control files are stored with Unix newlines and in UTF-8, even
193
if the platform or locale defaults are different.
196
fn = self.controlfilename(file_or_path)
198
if mode == 'rb' or mode == 'wb':
199
return file(fn, mode)
200
elif mode == 'r' or mode == 'w':
201
# open in binary mode anyhow so there's no newline translation;
202
# codecs uses line buffering by default; don't want that.
204
return codecs.open(fn, mode + 'b', 'utf-8',
207
raise BzrError("invalid controlfile mode %r" % mode)
211
def _make_control(self):
212
os.mkdir(self.controlfilename([]))
213
self.controlfile('README', 'w').write(
214
"This is a Bazaar-NG control directory.\n"
215
"Do not change any files in this directory.")
216
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
217
for d in ('text-store', 'inventory-store', 'revision-store'):
218
os.mkdir(self.controlfilename(d))
219
for f in ('revision-history', 'merged-patches',
220
'pending-merged-patches', 'branch-name',
222
self.controlfile(f, 'w').write('')
223
mutter('created control directory in ' + self.base)
224
Inventory().write_xml(self.controlfile('inventory','w'))
227
def _check_format(self):
228
"""Check this branch format is supported.
230
The current tool only supports the current unstable format.
232
In the future, we might need different in-memory Branch
233
classes to support downlevel branches. But not yet.
235
# This ignores newlines so that we can open branches created
236
# on Windows from Linux and so on. I think it might be better
237
# to always make all internal files in unix format.
238
fmt = self.controlfile('branch-format', 'r').read()
239
fmt.replace('\r\n', '')
240
if fmt != BZR_BRANCH_FORMAT:
241
bailout('sorry, branch format %r not supported' % fmt,
242
['use a different bzr version',
243
'or remove the .bzr directory and "bzr init" again'])
246
def read_working_inventory(self):
247
"""Read the working inventory."""
248
self._need_readlock()
250
# ElementTree does its own conversion from UTF-8, so open in
252
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
253
mutter("loaded inventory of %d items in %f"
254
% (len(inv), time.time() - before))
258
def _write_inventory(self, inv):
259
"""Update the working inventory.
261
That is to say, the inventory describing changes underway, that
262
will be committed to the next revision.
264
self._need_writelock()
265
## TODO: factor out to atomicfile? is rename safe on windows?
266
## TODO: Maybe some kind of clean/dirty marker on inventory?
267
tmpfname = self.controlfilename('inventory.tmp')
268
tmpf = file(tmpfname, 'wb')
271
inv_fname = self.controlfilename('inventory')
272
if sys.platform == 'win32':
274
os.rename(tmpfname, inv_fname)
275
mutter('wrote working inventory')
278
inventory = property(read_working_inventory, _write_inventory, None,
279
"""Inventory for the working copy.""")
282
def add(self, files, verbose=False):
283
"""Make files versioned.
285
Note that the command line normally calls smart_add instead.
287
This puts the files in the Added state, so that they will be
288
recorded by the next commit.
290
TODO: Perhaps have an option to add the ids even if the files do
293
TODO: Perhaps return the ids of the files? But then again it
294
is easy to retrieve them if they're needed.
296
TODO: Option to specify file id.
298
TODO: Adding a directory should optionally recurse down and
299
add all non-ignored children. Perhaps do that in a
302
>>> b = ScratchBranch(files=['foo'])
303
>>> 'foo' in b.unknowns()
308
>>> 'foo' in b.unknowns()
310
>>> bool(b.inventory.path2id('foo'))
316
Traceback (most recent call last):
318
BzrError: ('foo is already versioned', [])
320
>>> b.add(['nothere'])
321
Traceback (most recent call last):
322
BzrError: ('cannot add: not a regular file or directory: nothere', [])
324
self._need_writelock()
326
# TODO: Re-adding a file that is removed in the working copy
327
# should probably put it back with the previous ID.
328
if isinstance(files, types.StringTypes):
331
inv = self.read_working_inventory()
333
if is_control_file(f):
334
bailout("cannot add control file %s" % quotefn(f))
339
bailout("cannot add top-level %r" % f)
341
fullpath = os.path.normpath(self.abspath(f))
344
kind = file_kind(fullpath)
346
# maybe something better?
347
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
349
if kind != 'file' and kind != 'directory':
350
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
352
file_id = gen_file_id(f)
353
inv.add_path(f, kind=kind, file_id=file_id)
356
show_status('A', kind, quotefn(f))
358
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
360
self._write_inventory(inv)
363
def print_file(self, file, revno):
364
"""Print `file` to stdout."""
365
self._need_readlock()
366
tree = self.revision_tree(self.lookup_revision(revno))
367
# use inventory as it was in that revision
368
file_id = tree.inventory.path2id(file)
370
bailout("%r is not present in revision %d" % (file, revno))
371
tree.print_file(file_id)
374
def remove(self, files, verbose=False):
375
"""Mark nominated files for removal from the inventory.
377
This does not remove their text. This does not run on
379
TODO: Refuse to remove modified files unless --force is given?
381
>>> b = ScratchBranch(files=['foo'])
383
>>> b.inventory.has_filename('foo')
386
>>> b.working_tree().has_filename('foo')
388
>>> b.inventory.has_filename('foo')
391
>>> b = ScratchBranch(files=['foo'])
396
>>> b.inventory.has_filename('foo')
398
>>> b.basis_tree().has_filename('foo')
400
>>> b.working_tree().has_filename('foo')
403
TODO: Do something useful with directories.
405
TODO: Should this remove the text or not? Tough call; not
406
removing may be useful and the user can just use use rm, and
407
is the opposite of add. Removing it is consistent with most
408
other tools. Maybe an option.
410
## TODO: Normalize names
411
## TODO: Remove nested loops; better scalability
412
self._need_writelock()
414
if isinstance(files, types.StringTypes):
417
tree = self.working_tree()
420
# do this before any modifications
424
bailout("cannot remove unversioned file %s" % quotefn(f))
425
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
427
# having remove it, it must be either ignored or unknown
428
if tree.is_ignored(f):
432
show_status(new_status, inv[fid].kind, quotefn(f))
435
self._write_inventory(inv)
439
"""Return all unknown files.
441
These are files in the working directory that are not versioned or
442
control files or ignored.
444
>>> b = ScratchBranch(files=['foo', 'foo~'])
445
>>> list(b.unknowns())
448
>>> list(b.unknowns())
451
>>> list(b.unknowns())
454
return self.working_tree().unknowns()
457
def commit(self, message, timestamp=None, timezone=None,
460
"""Commit working copy as a new revision.
462
The basic approach is to add all the file texts into the
463
store, then the inventory, then make a new revision pointing
464
to that inventory and store that.
466
This is not quite safe if the working copy changes during the
467
commit; for the moment that is simply not allowed. A better
468
approach is to make a temporary copy of the files before
469
computing their hashes, and then add those hashes in turn to
470
the inventory. This should mean at least that there are no
471
broken hash pointers. There is no way we can get a snapshot
472
of the whole directory at an instant. This would also have to
473
be robust against files disappearing, moving, etc. So the
474
whole thing is a bit hard.
476
timestamp -- if not None, seconds-since-epoch for a
477
postdated/predated commit.
479
self._need_writelock()
481
## TODO: Show branch names
483
# TODO: Don't commit if there are no changes, unless forced?
485
# First walk over the working inventory; and both update that
486
# and also build a new revision inventory. The revision
487
# inventory needs to hold the text-id, sha1 and size of the
488
# actual file versions committed in the revision. (These are
489
# not present in the working inventory.) We also need to
490
# detect missing/deleted files, and remove them from the
493
work_inv = self.read_working_inventory()
495
basis = self.basis_tree()
496
basis_inv = basis.inventory
498
for path, entry in work_inv.iter_entries():
499
## TODO: Cope with files that have gone missing.
501
## TODO: Check that the file kind has not changed from the previous
502
## revision of this file (if any).
506
p = self.abspath(path)
507
file_id = entry.file_id
508
mutter('commit prep file %s, id %r ' % (p, file_id))
510
if not os.path.exists(p):
511
mutter(" file is missing, removing from inventory")
513
show_status('D', entry.kind, quotefn(path))
514
missing_ids.append(file_id)
517
# TODO: Handle files that have been deleted
519
# TODO: Maybe a special case for empty files? Seems a
520
# waste to store them many times.
524
if basis_inv.has_id(file_id):
525
old_kind = basis_inv[file_id].kind
526
if old_kind != entry.kind:
527
bailout("entry %r changed kind from %r to %r"
528
% (file_id, old_kind, entry.kind))
530
if entry.kind == 'directory':
532
bailout("%s is entered as directory but not a directory" % quotefn(p))
533
elif entry.kind == 'file':
535
bailout("%s is entered as file but is not a file" % quotefn(p))
537
content = file(p, 'rb').read()
539
entry.text_sha1 = sha_string(content)
540
entry.text_size = len(content)
542
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
544
and (old_ie.text_size == entry.text_size)
545
and (old_ie.text_sha1 == entry.text_sha1)):
546
## assert content == basis.get_file(file_id).read()
547
entry.text_id = basis_inv[file_id].text_id
548
mutter(' unchanged from previous text_id {%s}' %
552
entry.text_id = gen_file_id(entry.name)
553
self.text_store.add(content, entry.text_id)
554
mutter(' stored with text_id {%s}' % entry.text_id)
558
elif (old_ie.name == entry.name
559
and old_ie.parent_id == entry.parent_id):
564
show_status(state, entry.kind, quotefn(path))
566
for file_id in missing_ids:
567
# have to do this later so we don't mess up the iterator.
568
# since parents may be removed before their children we
571
# FIXME: There's probably a better way to do this; perhaps
572
# the workingtree should know how to filter itself.
573
if work_inv.has_id(file_id):
574
del work_inv[file_id]
577
inv_id = rev_id = _gen_revision_id(time.time())
579
inv_tmp = tempfile.TemporaryFile()
580
inv.write_xml(inv_tmp)
582
self.inventory_store.add(inv_tmp, inv_id)
583
mutter('new inventory_id is {%s}' % inv_id)
585
self._write_inventory(work_inv)
587
if timestamp == None:
588
timestamp = time.time()
590
if committer == None:
591
committer = username()
594
timezone = local_time_offset()
596
mutter("building commit log message")
597
rev = Revision(timestamp=timestamp,
600
precursor = self.last_patch(),
605
rev_tmp = tempfile.TemporaryFile()
606
rev.write_xml(rev_tmp)
608
self.revision_store.add(rev_tmp, rev_id)
609
mutter("new revision_id is {%s}" % rev_id)
611
## XXX: Everything up to here can simply be orphaned if we abort
612
## the commit; it will leave junk files behind but that doesn't
615
## TODO: Read back the just-generated changeset, and make sure it
616
## applies and recreates the right state.
618
## TODO: Also calculate and store the inventory SHA1
619
mutter("committing patch r%d" % (self.revno() + 1))
622
self.append_revision(rev_id)
625
note("commited r%d" % self.revno())
628
def append_revision(self, revision_id):
629
mutter("add {%s} to revision-history" % revision_id)
630
rev_history = self.revision_history()
632
tmprhname = self.controlfilename('revision-history.tmp')
633
rhname = self.controlfilename('revision-history')
635
f = file(tmprhname, 'wt')
636
rev_history.append(revision_id)
637
f.write('\n'.join(rev_history))
641
if sys.platform == 'win32':
643
os.rename(tmprhname, rhname)
647
def get_revision(self, revision_id):
648
"""Return the Revision object for a named revision"""
649
self._need_readlock()
650
r = Revision.read_xml(self.revision_store[revision_id])
651
assert r.revision_id == revision_id
655
def get_inventory(self, inventory_id):
656
"""Get Inventory object by hash.
658
TODO: Perhaps for this and similar methods, take a revision
659
parameter which can be either an integer revno or a
661
self._need_readlock()
662
i = Inventory.read_xml(self.inventory_store[inventory_id])
666
def get_revision_inventory(self, revision_id):
667
"""Return inventory of a past revision."""
668
self._need_readlock()
669
if revision_id == None:
672
return self.get_inventory(self.get_revision(revision_id).inventory_id)
675
def revision_history(self):
676
"""Return sequence of revision hashes on to this branch.
678
>>> ScratchBranch().revision_history()
681
self._need_readlock()
682
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
686
"""Return current revision number for this branch.
688
That is equivalent to the number of revisions committed to
691
>>> b = ScratchBranch()
694
>>> b.commit('no foo')
698
return len(self.revision_history())
701
def last_patch(self):
702
"""Return last patch hash, or None if no history.
704
>>> ScratchBranch().last_patch() == None
707
ph = self.revision_history()
714
def lookup_revision(self, revno):
715
"""Return revision hash for revision number."""
720
# list is 0-based; revisions are 1-based
721
return self.revision_history()[revno-1]
723
raise BzrError("no such revision %s" % revno)
726
def revision_tree(self, revision_id):
727
"""Return Tree for a revision on this branch.
729
`revision_id` may be None for the null revision, in which case
730
an `EmptyTree` is returned."""
731
self._need_readlock()
732
if revision_id == None:
735
inv = self.get_revision_inventory(revision_id)
736
return RevisionTree(self.text_store, inv)
739
def working_tree(self):
740
"""Return a `Tree` for the working copy."""
741
return WorkingTree(self.base, self.read_working_inventory())
744
def basis_tree(self):
745
"""Return `Tree` object for last revision.
747
If there are no revisions yet, return an `EmptyTree`.
749
>>> b = ScratchBranch(files=['foo'])
750
>>> b.basis_tree().has_filename('foo')
752
>>> b.working_tree().has_filename('foo')
755
>>> b.commit('add foo')
756
>>> b.basis_tree().has_filename('foo')
759
r = self.last_patch()
763
return RevisionTree(self.text_store, self.get_revision_inventory(r))
767
def write_log(self, show_timezone='original', verbose=False):
768
"""Write out human-readable log of commits to this branch
770
utc -- If true, show dates in universal time, not local time."""
771
self._need_readlock()
772
## TODO: Option to choose either original, utc or local timezone
775
for p in self.revision_history():
777
print 'revno:', revno
778
## TODO: Show hash if --id is given.
779
##print 'revision-hash:', p
780
rev = self.get_revision(p)
781
print 'committer:', rev.committer
782
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
785
## opportunistic consistency check, same as check_patch_chaining
786
if rev.precursor != precursor:
787
bailout("mismatched precursor!")
791
print ' (no message)'
793
for l in rev.message.split('\n'):
796
if verbose == True and precursor != None:
797
print 'changed files:'
798
tree = self.revision_tree(p)
799
prevtree = self.revision_tree(precursor)
801
for file_state, fid, old_name, new_name, kind in \
802
diff_trees(prevtree, tree, ):
803
if file_state == 'A' or file_state == 'M':
804
show_status(file_state, kind, new_name)
805
elif file_state == 'D':
806
show_status(file_state, kind, old_name)
807
elif file_state == 'R':
808
show_status(file_state, kind,
809
old_name + ' => ' + new_name)
815
def rename_one(self, from_rel, to_rel):
818
This can change the directory or the filename or both.
820
self._need_writelock()
821
tree = self.working_tree()
823
if not tree.has_filename(from_rel):
824
bailout("can't rename: old working file %r does not exist" % from_rel)
825
if tree.has_filename(to_rel):
826
bailout("can't rename: new working file %r already exists" % to_rel)
828
file_id = inv.path2id(from_rel)
830
bailout("can't rename: old name %r is not versioned" % from_rel)
832
if inv.path2id(to_rel):
833
bailout("can't rename: new name %r is already versioned" % to_rel)
835
to_dir, to_tail = os.path.split(to_rel)
836
to_dir_id = inv.path2id(to_dir)
837
if to_dir_id == None and to_dir != '':
838
bailout("can't determine destination directory id for %r" % to_dir)
840
mutter("rename_one:")
841
mutter(" file_id {%s}" % file_id)
842
mutter(" from_rel %r" % from_rel)
843
mutter(" to_rel %r" % to_rel)
844
mutter(" to_dir %r" % to_dir)
845
mutter(" to_dir_id {%s}" % to_dir_id)
847
inv.rename(file_id, to_dir_id, to_tail)
849
print "%s => %s" % (from_rel, to_rel)
851
from_abs = self.abspath(from_rel)
852
to_abs = self.abspath(to_rel)
854
os.rename(from_abs, to_abs)
856
bailout("failed to rename %r to %r: %s"
857
% (from_abs, to_abs, e[1]),
858
["rename rolled back"])
860
self._write_inventory(inv)
864
def move(self, from_paths, to_name):
867
to_name must exist as a versioned directory.
869
If to_name exists and is a directory, the files are moved into
870
it, keeping their old names. If it is a directory,
872
Note that to_name is only the last component of the new name;
873
this doesn't change the directory.
875
self._need_writelock()
876
## TODO: Option to move IDs only
877
assert not isinstance(from_paths, basestring)
878
tree = self.working_tree()
880
to_abs = self.abspath(to_name)
881
if not isdir(to_abs):
882
bailout("destination %r is not a directory" % to_abs)
883
if not tree.has_filename(to_name):
884
bailout("destination %r not in working directory" % to_abs)
885
to_dir_id = inv.path2id(to_name)
886
if to_dir_id == None and to_name != '':
887
bailout("destination %r is not a versioned directory" % to_name)
888
to_dir_ie = inv[to_dir_id]
889
if to_dir_ie.kind not in ('directory', 'root_directory'):
890
bailout("destination %r is not a directory" % to_abs)
892
to_idpath = Set(inv.get_idpath(to_dir_id))
895
if not tree.has_filename(f):
896
bailout("%r does not exist in working tree" % f)
897
f_id = inv.path2id(f)
899
bailout("%r is not versioned" % f)
900
name_tail = splitpath(f)[-1]
901
dest_path = appendpath(to_name, name_tail)
902
if tree.has_filename(dest_path):
903
bailout("destination %r already exists" % dest_path)
904
if f_id in to_idpath:
905
bailout("can't move %r to a subdirectory of itself" % f)
907
# OK, so there's a race here, it's possible that someone will
908
# create a file in this interval and then the rename might be
909
# left half-done. But we should have caught most problems.
912
name_tail = splitpath(f)[-1]
913
dest_path = appendpath(to_name, name_tail)
914
print "%s => %s" % (f, dest_path)
915
inv.rename(inv.path2id(f), to_dir_id, name_tail)
917
os.rename(self.abspath(f), self.abspath(dest_path))
919
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
920
["rename rolled back"])
922
self._write_inventory(inv)
926
def show_status(self, show_all=False):
927
"""Display single-line status for non-ignored working files.
929
The list is show sorted in order by file name.
931
>>> b = ScratchBranch(files=['foo', 'foo~'])
937
>>> b.commit("add foo")
939
>>> os.unlink(b.abspath('foo'))
943
TODO: Get state for single files.
945
self._need_readlock()
947
# We have to build everything into a list first so that it can
948
# sorted by name, incorporating all the different sources.
950
# FIXME: Rather than getting things in random order and then sorting,
951
# just step through in order.
953
# Interesting case: the old ID for a file has been removed,
954
# but a new file has been created under that name.
956
old = self.basis_tree()
957
new = self.working_tree()
959
for fs, fid, oldname, newname, kind in diff_trees(old, new):
961
show_status(fs, kind,
962
oldname + ' => ' + newname)
963
elif fs == 'A' or fs == 'M':
964
show_status(fs, kind, newname)
966
show_status(fs, kind, oldname)
969
show_status(fs, kind, newname)
972
show_status(fs, kind, newname)
974
show_status(fs, kind, newname)
976
bailout("weird file state %r" % ((fs, fid),))
980
class ScratchBranch(Branch):
981
"""Special test class: a branch that cleans up after itself.
983
>>> b = ScratchBranch()
991
def __init__(self, files=[], dirs=[]):
992
"""Make a test branch.
994
This creates a temporary directory and runs init-tree in it.
996
If any files are listed, they are created in the working copy.
998
Branch.__init__(self, tempfile.mkdtemp(), init=True)
1000
os.mkdir(self.abspath(d))
1003
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1007
"""Destroy the test branch, removing the scratch directory."""
1009
shutil.rmtree(self.base)
1011
# Work around for shutil.rmtree failing on Windows when
1012
# readonly files are encountered
1013
for root, dirs, files in os.walk(self.base, topdown=False):
1015
os.chmod(os.path.join(root, name), 0700)
1016
shutil.rmtree(self.base)
1020
######################################################################
1024
def is_control_file(filename):
1025
## FIXME: better check
1026
filename = os.path.normpath(filename)
1027
while filename != '':
1028
head, tail = os.path.split(filename)
1029
## mutter('check %r for control file' % ((head, tail), ))
1030
if tail == bzrlib.BZRDIR:
1032
if filename == head:
1039
def _gen_revision_id(when):
1040
"""Return new revision-id."""
1041
s = '%s-%s-' % (user_email(), compact_date(when))
1042
s += hexlify(rand_bytes(8))
1046
def gen_file_id(name):
1047
"""Return new file id.
1049
This should probably generate proper UUIDs, but for the moment we
1050
cope with just randomness because running uuidgen every time is
1052
idx = name.rfind('/')
1054
name = name[idx+1 : ]
1055
idx = name.rfind('\\')
1057
name = name[idx+1 : ]
1059
name = name.lstrip('.')
1061
s = hexlify(rand_bytes(8))
1062
return '-'.join((name, compact_date(time.time()), s))