26
26
from inventory import InventoryEntry, Inventory
27
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
28
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
joinpath, sha_string, file_kind, local_time_offset, appendpath
29
joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath
30
30
from store import ImmutableStore
31
31
from revision import Revision
32
from errors import bailout, BzrError
32
from errors import BzrError
33
33
from textui import show_status
35
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
129
184
__repr__ = __str__
133
def lock(self, mode='w'):
134
"""Lock the on-disk branch, excluding other processes."""
140
om = os.O_WRONLY | os.O_CREAT
145
raise BzrError("invalid locking mode %r" % mode)
148
lockfile = os.open(self.controlfilename('branch-lock'), om)
150
if e.errno == errno.ENOENT:
151
# might not exist on branches from <0.0.4
152
self.controlfile('branch-lock', 'w').close()
153
lockfile = os.open(self.controlfilename('branch-lock'), om)
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'
157
fcntl.lockf(lockfile, lm)
159
fcntl.lockf(lockfile, fcntl.LOCK_UN)
161
self._lockmode = None
163
self._lockmode = mode
165
warning("please write a locking method for platform %r" % sys.platform)
167
self._lockmode = None
169
self._lockmode = mode
172
def _need_readlock(self):
173
if self._lockmode not in ['r', 'w']:
174
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
176
def _need_writelock(self):
177
if self._lockmode not in ['w']:
178
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
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
181
238
def abspath(self, name):
260
311
fmt = self.controlfile('branch-format', 'r').read()
261
312
fmt.replace('\r\n', '')
262
313
if fmt != BZR_BRANCH_FORMAT:
263
bailout('sorry, branch format %r not supported' % fmt,
264
['use a different bzr version',
265
'or remove the .bzr directory and "bzr init" again'])
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'])
268
320
def read_working_inventory(self):
269
321
"""Read the working inventory."""
270
self._need_readlock()
271
322
before = time.time()
272
323
# ElementTree does its own conversion from UTF-8, so open in
274
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
275
mutter("loaded inventory of %d items in %f"
276
% (len(inv), time.time() - before))
327
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
328
mutter("loaded inventory of %d items in %f"
329
% (len(inv), time.time() - before))
280
335
def _write_inventory(self, inv):
281
336
"""Update the working inventory.
283
338
That is to say, the inventory describing changes underway, that
284
339
will be committed to the next revision.
286
self._need_writelock()
287
## TODO: factor out to atomicfile? is rename safe on windows?
288
## TODO: Maybe some kind of clean/dirty marker on inventory?
289
tmpfname = self.controlfilename('inventory.tmp')
290
tmpf = file(tmpfname, 'wb')
293
inv_fname = self.controlfilename('inventory')
294
if sys.platform == 'win32':
296
os.rename(tmpfname, inv_fname)
343
from bzrlib.atomicfile import AtomicFile
345
f = AtomicFile(self.controlfilename('inventory'), 'wb')
297
354
mutter('wrote working inventory')
300
357
inventory = property(read_working_inventory, _write_inventory, None,
301
358
"""Inventory for the working copy.""")
309
366
This puts the files in the Added state, so that they will be
310
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.
312
377
TODO: Perhaps have an option to add the ids even if the files do
315
380
TODO: Perhaps return the ids of the files? But then again it
316
is easy to retrieve them if they're needed.
318
TODO: Option to specify file id.
381
is easy to retrieve them if they're needed.
320
383
TODO: Adding a directory should optionally recurse down and
321
add all non-ignored children. Perhaps do that in a
384
add all non-ignored children. Perhaps do that in a
324
self._need_writelock()
326
387
# TODO: Re-adding a file that is removed in the working copy
327
388
# should probably put it back with the previous ID.
328
389
if isinstance(files, types.StringTypes):
335
396
ids = [None] * len(files)
337
398
assert(len(ids) == len(files))
339
inv = self.read_working_inventory()
340
for f,file_id in zip(files, ids):
341
if is_control_file(f):
342
bailout("cannot add control file %s" % quotefn(f))
347
bailout("cannot add top-level %r" % f)
349
fullpath = os.path.normpath(self.abspath(f))
352
kind = file_kind(fullpath)
354
# maybe something better?
355
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
357
if kind != 'file' and kind != 'directory':
358
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
361
file_id = gen_file_id(f)
362
inv.add_path(f, kind=kind, file_id=file_id)
365
show_status('A', kind, quotefn(f))
367
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
369
self._write_inventory(inv)
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)
372
437
def print_file(self, file, revno):
373
438
"""Print `file` to stdout."""
374
self._need_readlock()
375
tree = self.revision_tree(self.lookup_revision(revno))
376
# use inventory as it was in that revision
377
file_id = tree.inventory.path2id(file)
379
bailout("%r is not present in revision %d" % (file, revno))
380
tree.print_file(file_id)
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)
383
451
def remove(self, files, verbose=False):
384
452
"""Mark nominated files for removal from the inventory.
397
465
## TODO: Normalize names
398
466
## TODO: Remove nested loops; better scalability
399
self._need_writelock()
401
467
if isinstance(files, types.StringTypes):
404
tree = self.working_tree()
407
# do this before any modifications
411
bailout("cannot remove unversioned file %s" % quotefn(f))
412
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
414
# having remove it, it must be either ignored or unknown
415
if tree.is_ignored(f):
419
show_status(new_status, inv[fid].kind, quotefn(f))
422
self._write_inventory(inv)
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
424
497
def set_inventory(self, new_inventory_list):
425
498
inv = Inventory()
426
499
for path, file_id, parent, kind in new_inventory_list:
453
526
def append_revision(self, revision_id):
527
from bzrlib.atomicfile import AtomicFile
454
529
mutter("add {%s} to revision-history" % revision_id)
455
rev_history = self.revision_history()
457
tmprhname = self.controlfilename('revision-history.tmp')
458
rhname = self.controlfilename('revision-history')
460
f = file(tmprhname, 'wt')
461
rev_history.append(revision_id)
462
f.write('\n'.join(rev_history))
466
if sys.platform == 'win32':
468
os.rename(tmprhname, rhname)
530
rev_history = self.revision_history() + [revision_id]
532
f = AtomicFile(self.controlfilename('revision-history'))
534
for rev_id in rev_history:
472
541
def get_revision(self, revision_id):
473
542
"""Return the Revision object for a named revision"""
474
self._need_readlock()
543
if not revision_id or not isinstance(revision_id, basestring):
544
raise ValueError('invalid revision-id: %r' % revision_id)
475
545
r = Revision.read_xml(self.revision_store[revision_id])
476
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])
480
560
def get_inventory(self, inventory_id):
481
561
"""Get Inventory object by hash.
503
586
>>> ScratchBranch().revision_history()
506
self._need_readlock()
507
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
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]
510
641
def enum_history(self, direction):
511
642
"""Return (revno, revision_id) for history of branch.
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
551
786
def commit(self, *args, **kw):
553
787
from bzrlib.commit import commit
554
788
commit(self, *args, **kw)
606
839
This can change the directory or the filename or both.
608
self._need_writelock()
609
tree = self.working_tree()
611
if not tree.has_filename(from_rel):
612
bailout("can't rename: old working file %r does not exist" % from_rel)
613
if tree.has_filename(to_rel):
614
bailout("can't rename: new working file %r already exists" % to_rel)
616
file_id = inv.path2id(from_rel)
618
bailout("can't rename: old name %r is not versioned" % from_rel)
620
if inv.path2id(to_rel):
621
bailout("can't rename: new name %r is already versioned" % to_rel)
623
to_dir, to_tail = os.path.split(to_rel)
624
to_dir_id = inv.path2id(to_dir)
625
if to_dir_id == None and to_dir != '':
626
bailout("can't determine destination directory id for %r" % to_dir)
628
mutter("rename_one:")
629
mutter(" file_id {%s}" % file_id)
630
mutter(" from_rel %r" % from_rel)
631
mutter(" to_rel %r" % to_rel)
632
mutter(" to_dir %r" % to_dir)
633
mutter(" to_dir_id {%s}" % to_dir_id)
635
inv.rename(file_id, to_dir_id, to_tail)
637
print "%s => %s" % (from_rel, to_rel)
639
from_abs = self.abspath(from_rel)
640
to_abs = self.abspath(to_rel)
642
os.rename(from_abs, to_abs)
644
bailout("failed to rename %r to %r: %s"
645
% (from_abs, to_abs, e[1]),
646
["rename rolled back"])
648
self._write_inventory(inv)
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)
652
887
def move(self, from_paths, to_name):
660
895
Note that to_name is only the last component of the new name;
661
896
this doesn't change the directory.
663
self._need_writelock()
664
## TODO: Option to move IDs only
665
assert not isinstance(from_paths, basestring)
666
tree = self.working_tree()
668
to_abs = self.abspath(to_name)
669
if not isdir(to_abs):
670
bailout("destination %r is not a directory" % to_abs)
671
if not tree.has_filename(to_name):
672
bailout("destination %r not in working directory" % to_abs)
673
to_dir_id = inv.path2id(to_name)
674
if to_dir_id == None and to_name != '':
675
bailout("destination %r is not a versioned directory" % to_name)
676
to_dir_ie = inv[to_dir_id]
677
if to_dir_ie.kind not in ('directory', 'root_directory'):
678
bailout("destination %r is not a directory" % to_abs)
680
to_idpath = inv.get_idpath(to_dir_id)
683
if not tree.has_filename(f):
684
bailout("%r does not exist in working tree" % f)
685
f_id = inv.path2id(f)
687
bailout("%r is not versioned" % f)
688
name_tail = splitpath(f)[-1]
689
dest_path = appendpath(to_name, name_tail)
690
if tree.has_filename(dest_path):
691
bailout("destination %r already exists" % dest_path)
692
if f_id in to_idpath:
693
bailout("can't move %r to a subdirectory of itself" % f)
695
# OK, so there's a race here, it's possible that someone will
696
# create a file in this interval and then the rename might be
697
# left half-done. But we should have caught most problems.
700
name_tail = splitpath(f)[-1]
701
dest_path = appendpath(to_name, name_tail)
702
print "%s => %s" % (f, dest_path)
703
inv.rename(inv.path2id(f), to_dir_id, name_tail)
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')
705
os.rename(self.abspath(f), self.abspath(dest_path))
707
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
708
["rename rolled back"])
710
self._write_inventory(inv)
989
f.write(old_tree.get_file(file_id).read())