23
25
from inventory import Inventory
24
26
from trace import mutter, note
25
from tree import Tree, EmptyTree, RevisionTree
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
26
28
from inventory import InventoryEntry, Inventory
27
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
30
32
from store import ImmutableStore
31
33
from revision import Revision
32
from errors import BzrError
34
from errors import bailout, BzrError
33
35
from textui import show_status
36
from diff import diff_trees
35
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
39
## 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
43
def find_branch_root(f=None):
78
44
"""Find the branch root enclosing f, or pwd.
80
f may be a filename or a URL.
82
46
It is not necessary that f exists.
84
48
Basically we keep looking up until we find the control directory or
175
119
__repr__ = __str__
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
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
# XXX: Old branches might not have the lock file, and
138
# won't get one until someone does a write-mode command on
139
# them or creates it by hand.
141
lockfile = os.open(self.controlfilename('branch-lock'), om)
142
fcntl.lockf(lockfile, lm)
144
fcntl.lockf(lockfile, fcntl.LOCK_UN)
146
self._lockmode = None
148
self._lockmode = mode
150
warning("please write a locking method for platform %r" % sys.platform)
152
self._lockmode = None
154
self._lockmode = mode
157
def _need_readlock(self):
158
if self._lockmode not in ['r', 'w']:
159
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
161
def _need_writelock(self):
162
if self._lockmode not in ['w']:
163
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
229
166
def abspath(self, name):
302
242
fmt = self.controlfile('branch-format', 'r').read()
303
243
fmt.replace('\r\n', '')
304
244
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'])
245
bailout('sorry, branch format %r not supported' % fmt,
246
['use a different bzr version',
247
'or remove the .bzr directory and "bzr init" again'])
311
250
def read_working_inventory(self):
312
251
"""Read the working inventory."""
252
self._need_readlock()
313
253
before = time.time()
314
254
# 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))
256
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
257
mutter("loaded inventory of %d items in %f"
258
% (len(inv), time.time() - before))
326
262
def _write_inventory(self, inv):
327
263
"""Update the working inventory.
354
291
This puts the files in the Added state, so that they will be
355
292
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
294
TODO: Perhaps have an option to add the ids even if the files do
368
297
TODO: Perhaps return the ids of the files? But then again it
369
is easy to retrieve them if they're needed.
298
is easy to retrieve them if they're needed.
300
TODO: Option to specify file id.
371
302
TODO: Adding a directory should optionally recurse down and
372
add all non-ignored children. Perhaps do that in a
303
add all non-ignored children. Perhaps do that in a
306
>>> b = ScratchBranch(files=['foo'])
307
>>> 'foo' in b.unknowns()
312
>>> 'foo' in b.unknowns()
314
>>> bool(b.inventory.path2id('foo'))
320
Traceback (most recent call last):
322
BzrError: ('foo is already versioned', [])
324
>>> b.add(['nothere'])
325
Traceback (most recent call last):
326
BzrError: ('cannot add: not a regular file or directory: nothere', [])
328
self._need_writelock()
375
330
# TODO: Re-adding a file that is removed in the working copy
376
331
# should probably put it back with the previous ID.
377
332
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)
335
inv = self.read_working_inventory()
337
if is_control_file(f):
338
bailout("cannot add control file %s" % quotefn(f))
343
bailout("cannot add top-level %r" % f)
345
fullpath = os.path.normpath(self.abspath(f))
348
kind = file_kind(fullpath)
350
# maybe something better?
351
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
353
if kind != 'file' and kind != 'directory':
354
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
356
file_id = gen_file_id(f)
357
inv.add_path(f, kind=kind, file_id=file_id)
360
show_status('A', kind, quotefn(f))
362
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
364
self._write_inventory(inv)
425
367
def print_file(self, file, revno):
426
368
"""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)
369
self._need_readlock()
370
tree = self.revision_tree(self.lookup_revision(revno))
371
# use inventory as it was in that revision
372
file_id = tree.inventory.path2id(file)
374
bailout("%r is not present in revision %d" % (file, revno))
375
tree.print_file(file_id)
439
378
def remove(self, files, verbose=False):
440
379
"""Mark nominated files for removal from the inventory.
453
414
## TODO: Normalize names
454
415
## TODO: Remove nested loops; better scalability
416
self._need_writelock()
455
418
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))
421
tree = self.working_tree()
424
# do this before any modifications
428
bailout("cannot remove unversioned file %s" % quotefn(f))
429
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
431
# having remove it, it must be either ignored or unknown
432
if tree.is_ignored(f):
436
show_status(new_status, inv[fid].kind, quotefn(f))
492
439
self._write_inventory(inv)
511
458
return self.working_tree().unknowns()
461
def commit(self, message, timestamp=None, timezone=None,
464
"""Commit working copy as a new revision.
466
The basic approach is to add all the file texts into the
467
store, then the inventory, then make a new revision pointing
468
to that inventory and store that.
470
This is not quite safe if the working copy changes during the
471
commit; for the moment that is simply not allowed. A better
472
approach is to make a temporary copy of the files before
473
computing their hashes, and then add those hashes in turn to
474
the inventory. This should mean at least that there are no
475
broken hash pointers. There is no way we can get a snapshot
476
of the whole directory at an instant. This would also have to
477
be robust against files disappearing, moving, etc. So the
478
whole thing is a bit hard.
480
timestamp -- if not None, seconds-since-epoch for a
481
postdated/predated commit.
483
self._need_writelock()
485
## TODO: Show branch names
487
# TODO: Don't commit if there are no changes, unless forced?
489
# First walk over the working inventory; and both update that
490
# and also build a new revision inventory. The revision
491
# inventory needs to hold the text-id, sha1 and size of the
492
# actual file versions committed in the revision. (These are
493
# not present in the working inventory.) We also need to
494
# detect missing/deleted files, and remove them from the
497
work_inv = self.read_working_inventory()
499
basis = self.basis_tree()
500
basis_inv = basis.inventory
502
for path, entry in work_inv.iter_entries():
503
## TODO: Cope with files that have gone missing.
505
## TODO: Check that the file kind has not changed from the previous
506
## revision of this file (if any).
510
p = self.abspath(path)
511
file_id = entry.file_id
512
mutter('commit prep file %s, id %r ' % (p, file_id))
514
if not os.path.exists(p):
515
mutter(" file is missing, removing from inventory")
517
show_status('D', entry.kind, quotefn(path))
518
missing_ids.append(file_id)
521
# TODO: Handle files that have been deleted
523
# TODO: Maybe a special case for empty files? Seems a
524
# waste to store them many times.
528
if basis_inv.has_id(file_id):
529
old_kind = basis_inv[file_id].kind
530
if old_kind != entry.kind:
531
bailout("entry %r changed kind from %r to %r"
532
% (file_id, old_kind, entry.kind))
534
if entry.kind == 'directory':
536
bailout("%s is entered as directory but not a directory" % quotefn(p))
537
elif entry.kind == 'file':
539
bailout("%s is entered as file but is not a file" % quotefn(p))
541
content = file(p, 'rb').read()
543
entry.text_sha1 = sha_string(content)
544
entry.text_size = len(content)
546
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
548
and (old_ie.text_size == entry.text_size)
549
and (old_ie.text_sha1 == entry.text_sha1)):
550
## assert content == basis.get_file(file_id).read()
551
entry.text_id = basis_inv[file_id].text_id
552
mutter(' unchanged from previous text_id {%s}' %
556
entry.text_id = gen_file_id(entry.name)
557
self.text_store.add(content, entry.text_id)
558
mutter(' stored with text_id {%s}' % entry.text_id)
562
elif (old_ie.name == entry.name
563
and old_ie.parent_id == entry.parent_id):
568
show_status(state, entry.kind, quotefn(path))
570
for file_id in missing_ids:
571
# have to do this later so we don't mess up the iterator.
572
# since parents may be removed before their children we
575
# FIXME: There's probably a better way to do this; perhaps
576
# the workingtree should know how to filter itself.
577
if work_inv.has_id(file_id):
578
del work_inv[file_id]
581
inv_id = rev_id = _gen_revision_id(time.time())
583
inv_tmp = tempfile.TemporaryFile()
584
inv.write_xml(inv_tmp)
586
self.inventory_store.add(inv_tmp, inv_id)
587
mutter('new inventory_id is {%s}' % inv_id)
589
self._write_inventory(work_inv)
591
if timestamp == None:
592
timestamp = time.time()
594
if committer == None:
595
committer = username()
598
timezone = local_time_offset()
600
mutter("building commit log message")
601
rev = Revision(timestamp=timestamp,
604
precursor = self.last_patch(),
609
rev_tmp = tempfile.TemporaryFile()
610
rev.write_xml(rev_tmp)
612
self.revision_store.add(rev_tmp, rev_id)
613
mutter("new revision_id is {%s}" % rev_id)
615
## XXX: Everything up to here can simply be orphaned if we abort
616
## the commit; it will leave junk files behind but that doesn't
619
## TODO: Read back the just-generated changeset, and make sure it
620
## applies and recreates the right state.
622
## TODO: Also calculate and store the inventory SHA1
623
mutter("committing patch r%d" % (self.revno() + 1))
626
self.append_revision(rev_id)
629
note("commited r%d" % self.revno())
514
632
def append_revision(self, revision_id):
515
633
mutter("add {%s} to revision-history" % revision_id)
516
634
rev_history = self.revision_history()
561
682
>>> ScratchBranch().revision_history()
566
return [l.rstrip('\r\n') for l in
567
self.controlfile('revision-history', 'r').readlines()]
572
def common_ancestor(self, other, self_revno=None, other_revno=None):
575
>>> sb = ScratchBranch(files=['foo', 'foo~'])
576
>>> sb.common_ancestor(sb) == (None, None)
578
>>> commit.commit(sb, "Committing first revision", verbose=False)
579
>>> sb.common_ancestor(sb)[0]
581
>>> clone = sb.clone()
582
>>> commit.commit(sb, "Committing second revision", verbose=False)
583
>>> sb.common_ancestor(sb)[0]
585
>>> sb.common_ancestor(clone)[0]
587
>>> commit.commit(clone, "Committing divergent second revision",
589
>>> sb.common_ancestor(clone)[0]
591
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
593
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
595
>>> clone2 = sb.clone()
596
>>> sb.common_ancestor(clone2)[0]
598
>>> sb.common_ancestor(clone2, self_revno=1)[0]
600
>>> sb.common_ancestor(clone2, other_revno=1)[0]
603
my_history = self.revision_history()
604
other_history = other.revision_history()
605
if self_revno is None:
606
self_revno = len(my_history)
607
if other_revno is None:
608
other_revno = len(other_history)
609
indices = range(min((self_revno, other_revno)))
612
if my_history[r] == other_history[r]:
613
return r+1, my_history[r]
616
def enum_history(self, direction):
617
"""Return (revno, revision_id) for history of branch.
620
'forward' is from earliest to latest
621
'reverse' is from latest to earliest
623
rh = self.revision_history()
624
if direction == 'forward':
629
elif direction == 'reverse':
635
raise ValueError('invalid history direction', direction)
685
self._need_readlock()
686
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
641
692
That is equivalent to the number of revisions committed to
695
>>> b = ScratchBranch()
698
>>> b.commit('no foo')
644
702
return len(self.revision_history())
647
705
def last_patch(self):
648
706
"""Return last patch hash, or None if no history.
708
>>> ScratchBranch().last_patch() == None
650
711
ph = self.revision_history()
657
def missing_revisions(self, other):
659
If self and other have not diverged, return a list of the revisions
660
present in other, but missing from self.
662
>>> from bzrlib.commit import commit
663
>>> bzrlib.trace.silent = True
664
>>> br1 = ScratchBranch()
665
>>> br2 = ScratchBranch()
666
>>> br1.missing_revisions(br2)
668
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
669
>>> br1.missing_revisions(br2)
671
>>> br2.missing_revisions(br1)
673
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
674
>>> br1.missing_revisions(br2)
676
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
677
>>> br1.missing_revisions(br2)
679
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
680
>>> br1.missing_revisions(br2)
681
Traceback (most recent call last):
682
DivergedBranches: These branches have diverged.
684
self_history = self.revision_history()
685
self_len = len(self_history)
686
other_history = other.revision_history()
687
other_len = len(other_history)
688
common_index = min(self_len, other_len) -1
689
if common_index >= 0 and \
690
self_history[common_index] != other_history[common_index]:
691
raise DivergedBranches(self, other)
692
if self_len < other_len:
693
return other_history[self_len:]
697
def update_revisions(self, other):
698
"""If self and other have not diverged, ensure self has all the
701
>>> from bzrlib.commit import commit
702
>>> bzrlib.trace.silent = True
703
>>> br1 = ScratchBranch(files=['foo', 'bar'])
706
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
707
>>> br2 = ScratchBranch()
708
>>> br2.update_revisions(br1)
712
>>> br2.revision_history()
714
>>> br2.update_revisions(br1)
718
>>> br1.text_store.total_size() == br2.text_store.total_size()
721
revision_ids = self.missing_revisions(other)
722
revisions = [other.get_revision(f) for f in revision_ids]
723
needed_texts = sets.Set()
724
for rev in revisions:
725
inv = other.get_inventory(str(rev.inventory_id))
726
for key, entry in inv.iter_entries():
727
if entry.text_id is None:
729
if entry.text_id not in self.text_store:
730
needed_texts.add(entry.text_id)
731
count = self.text_store.copy_multi(other.text_store, needed_texts)
732
print "Added %d texts." % count
733
inventory_ids = [ f.inventory_id for f in revisions ]
734
count = self.inventory_store.copy_multi(other.inventory_store,
736
print "Added %d inventories." % count
737
revision_ids = [ f.revision_id for f in revisions]
738
count = self.revision_store.copy_multi(other.revision_store,
740
for revision_id in revision_ids:
741
self.append_revision(revision_id)
742
print "Added %d revisions." % count
745
def commit(self, *args, **kw):
747
from bzrlib.commit import commit
748
commit(self, *args, **kw)
751
718
def lookup_revision(self, revno):
771
def write_log(self, show_timezone='original', verbose=False):
772
"""Write out human-readable log of commits to this branch
774
utc -- If true, show dates in universal time, not local time."""
775
self._need_readlock()
776
## TODO: Option to choose either original, utc or local timezone
779
for p in self.revision_history():
781
print 'revno:', revno
782
## TODO: Show hash if --id is given.
783
##print 'revision-hash:', p
784
rev = self.get_revision(p)
785
print 'committer:', rev.committer
786
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
789
## opportunistic consistency check, same as check_patch_chaining
790
if rev.precursor != precursor:
791
bailout("mismatched precursor!")
795
print ' (no message)'
797
for l in rev.message.split('\n'):
800
if verbose == True and precursor != None:
801
print 'changed files:'
802
tree = self.revision_tree(p)
803
prevtree = self.revision_tree(precursor)
805
for file_state, fid, old_name, new_name, kind in \
806
diff_trees(prevtree, tree, ):
807
if file_state == 'A' or file_state == 'M':
808
show_status(file_state, kind, new_name)
809
elif file_state == 'D':
810
show_status(file_state, kind, old_name)
811
elif file_state == 'R':
812
show_status(file_state, kind,
813
old_name + ' => ' + new_name)
796
819
def rename_one(self, from_rel, to_rel):
797
820
"""Rename one file.
799
822
This can change the directory or the filename or both.
824
self._need_writelock()
825
tree = self.working_tree()
827
if not tree.has_filename(from_rel):
828
bailout("can't rename: old working file %r does not exist" % from_rel)
829
if tree.has_filename(to_rel):
830
bailout("can't rename: new working file %r already exists" % to_rel)
832
file_id = inv.path2id(from_rel)
834
bailout("can't rename: old name %r is not versioned" % from_rel)
836
if inv.path2id(to_rel):
837
bailout("can't rename: new name %r is already versioned" % to_rel)
839
to_dir, to_tail = os.path.split(to_rel)
840
to_dir_id = inv.path2id(to_dir)
841
if to_dir_id == None and to_dir != '':
842
bailout("can't determine destination directory id for %r" % to_dir)
844
mutter("rename_one:")
845
mutter(" file_id {%s}" % file_id)
846
mutter(" from_rel %r" % from_rel)
847
mutter(" to_rel %r" % to_rel)
848
mutter(" to_dir %r" % to_dir)
849
mutter(" to_dir_id {%s}" % to_dir_id)
851
inv.rename(file_id, to_dir_id, to_tail)
853
print "%s => %s" % (from_rel, to_rel)
855
from_abs = self.abspath(from_rel)
856
to_abs = self.abspath(to_rel)
803
tree = self.working_tree()
805
if not tree.has_filename(from_rel):
806
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
807
if tree.has_filename(to_rel):
808
raise BzrError("can't rename: new working file %r already exists" % to_rel)
810
file_id = inv.path2id(from_rel)
812
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
814
if inv.path2id(to_rel):
815
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
817
to_dir, to_tail = os.path.split(to_rel)
818
to_dir_id = inv.path2id(to_dir)
819
if to_dir_id == None and to_dir != '':
820
raise BzrError("can't determine destination directory id for %r" % to_dir)
822
mutter("rename_one:")
823
mutter(" file_id {%s}" % file_id)
824
mutter(" from_rel %r" % from_rel)
825
mutter(" to_rel %r" % to_rel)
826
mutter(" to_dir %r" % to_dir)
827
mutter(" to_dir_id {%s}" % to_dir_id)
829
inv.rename(file_id, to_dir_id, to_tail)
831
print "%s => %s" % (from_rel, to_rel)
833
from_abs = self.abspath(from_rel)
834
to_abs = self.abspath(to_rel)
836
os.rename(from_abs, to_abs)
838
raise BzrError("failed to rename %r to %r: %s"
839
% (from_abs, to_abs, e[1]),
840
["rename rolled back"])
842
self._write_inventory(inv)
858
os.rename(from_abs, to_abs)
860
bailout("failed to rename %r to %r: %s"
861
% (from_abs, to_abs, e[1]),
862
["rename rolled back"])
864
self._write_inventory(inv)
847
868
def move(self, from_paths, to_name):
855
876
Note that to_name is only the last component of the new name;
856
877
this doesn't change the directory.
860
## TODO: Option to move IDs only
861
assert not isinstance(from_paths, basestring)
862
tree = self.working_tree()
864
to_abs = self.abspath(to_name)
865
if not isdir(to_abs):
866
raise BzrError("destination %r is not a directory" % to_abs)
867
if not tree.has_filename(to_name):
868
raise BzrError("destination %r not in working directory" % to_abs)
869
to_dir_id = inv.path2id(to_name)
870
if to_dir_id == None and to_name != '':
871
raise BzrError("destination %r is not a versioned directory" % to_name)
872
to_dir_ie = inv[to_dir_id]
873
if to_dir_ie.kind not in ('directory', 'root_directory'):
874
raise BzrError("destination %r is not a directory" % to_abs)
876
to_idpath = inv.get_idpath(to_dir_id)
879
if not tree.has_filename(f):
880
raise BzrError("%r does not exist in working tree" % f)
881
f_id = inv.path2id(f)
883
raise BzrError("%r is not versioned" % f)
884
name_tail = splitpath(f)[-1]
885
dest_path = appendpath(to_name, name_tail)
886
if tree.has_filename(dest_path):
887
raise BzrError("destination %r already exists" % dest_path)
888
if f_id in to_idpath:
889
raise BzrError("can't move %r to a subdirectory of itself" % f)
891
# OK, so there's a race here, it's possible that someone will
892
# create a file in this interval and then the rename might be
893
# left half-done. But we should have caught most problems.
896
name_tail = splitpath(f)[-1]
897
dest_path = appendpath(to_name, name_tail)
898
print "%s => %s" % (f, dest_path)
899
inv.rename(inv.path2id(f), to_dir_id, name_tail)
901
os.rename(self.abspath(f), self.abspath(dest_path))
903
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
904
["rename rolled back"])
906
self._write_inventory(inv)
879
self._need_writelock()
880
## TODO: Option to move IDs only
881
assert not isinstance(from_paths, basestring)
882
tree = self.working_tree()
884
to_abs = self.abspath(to_name)
885
if not isdir(to_abs):
886
bailout("destination %r is not a directory" % to_abs)
887
if not tree.has_filename(to_name):
888
bailout("destination %r not in working directory" % to_abs)
889
to_dir_id = inv.path2id(to_name)
890
if to_dir_id == None and to_name != '':
891
bailout("destination %r is not a versioned directory" % to_name)
892
to_dir_ie = inv[to_dir_id]
893
if to_dir_ie.kind not in ('directory', 'root_directory'):
894
bailout("destination %r is not a directory" % to_abs)
896
to_idpath = Set(inv.get_idpath(to_dir_id))
899
if not tree.has_filename(f):
900
bailout("%r does not exist in working tree" % f)
901
f_id = inv.path2id(f)
903
bailout("%r is not versioned" % f)
904
name_tail = splitpath(f)[-1]
905
dest_path = appendpath(to_name, name_tail)
906
if tree.has_filename(dest_path):
907
bailout("destination %r already exists" % dest_path)
908
if f_id in to_idpath:
909
bailout("can't move %r to a subdirectory of itself" % f)
911
# OK, so there's a race here, it's possible that someone will
912
# create a file in this interval and then the rename might be
913
# left half-done. But we should have caught most problems.
916
name_tail = splitpath(f)[-1]
917
dest_path = appendpath(to_name, name_tail)
918
print "%s => %s" % (f, dest_path)
919
inv.rename(inv.path2id(f), to_dir_id, name_tail)
921
os.rename(self.abspath(f), self.abspath(dest_path))
923
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
924
["rename rolled back"])
926
self._write_inventory(inv)
930
def show_status(self, show_all=False):
931
"""Display single-line status for non-ignored working files.
933
The list is show sorted in order by file name.
935
>>> b = ScratchBranch(files=['foo', 'foo~'])
941
>>> b.commit("add foo")
943
>>> os.unlink(b.abspath('foo'))
947
TODO: Get state for single files.
949
self._need_readlock()
951
# We have to build everything into a list first so that it can
952
# sorted by name, incorporating all the different sources.
954
# FIXME: Rather than getting things in random order and then sorting,
955
# just step through in order.
957
# Interesting case: the old ID for a file has been removed,
958
# but a new file has been created under that name.
960
old = self.basis_tree()
961
new = self.working_tree()
963
for fs, fid, oldname, newname, kind in diff_trees(old, new):
965
show_status(fs, kind,
966
oldname + ' => ' + newname)
967
elif fs == 'A' or fs == 'M':
968
show_status(fs, kind, newname)
970
show_status(fs, kind, oldname)
973
show_status(fs, kind, newname)
976
show_status(fs, kind, newname)
978
show_status(fs, kind, newname)
980
bailout("weird file state %r" % ((fs, fid),))
912
984
class ScratchBranch(Branch):
939
1007
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
944
>>> orig = ScratchBranch(files=["file1", "file2"])
945
>>> clone = orig.clone()
946
>>> os.path.samefile(orig.base, clone.base)
948
>>> os.path.isfile(os.path.join(clone.base, "file1"))
951
base = tempfile.mkdtemp()
953
shutil.copytree(self.base, base, symlinks=True)
954
return ScratchBranch(base=base)
956
1010
def __del__(self):
960
1011
"""Destroy the test branch, removing the scratch directory."""
963
mutter("delete ScratchBranch %s" % self.base)
964
shutil.rmtree(self.base)
1013
shutil.rmtree(self.base)
966
1015
# Work around for shutil.rmtree failing on Windows when
967
1016
# readonly files are encountered
968
mutter("hit exception in destroying ScratchBranch: %s" % e)
969
1017
for root, dirs, files in os.walk(self.base, topdown=False):
970
1018
for name in files:
971
1019
os.chmod(os.path.join(root, name), 0700)
972
1020
shutil.rmtree(self.base)