74
74
"""Branch holding a history of revisions.
76
TODO: Perhaps use different stores for different classes of object,
77
so that we can keep track of how much space each one uses,
78
or garbage-collect them.
80
TODO: Add a RemoteBranch subclass. For the basic case of read-only
81
HTTP access this should be very easy by,
82
just redirecting controlfile access into HTTP requests.
83
We would need a RemoteStore working similarly.
85
TODO: Keep the on-disk branch locked while the object exists.
77
Base directory of the branch.
89
def __init__(self, base, init=False, find_root=True):
81
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
90
82
"""Create new branch object at a particular location.
92
84
base -- Base directory for the branch.
113
105
['use "bzr init" to initialize a new working tree',
114
106
'current bzr can only operate from top-of-tree'])
115
107
self._check_format()
117
110
self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
111
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
126
119
__repr__ = __str__
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)
138
lockfile = os.open(self.controlfilename('branch-lock'), om)
140
if e.errno == errno.ENOENT:
141
# might not exist on branches from <0.0.4
142
self.controlfile('branch-lock', 'w').close()
143
lockfile = os.open(self.controlfilename('branch-lock'), om)
147
fcntl.lockf(lockfile, lm)
149
fcntl.lockf(lockfile, fcntl.LOCK_UN)
151
self._lockmode = None
153
self._lockmode = mode
155
warning("please write a locking method for platform %r" % sys.platform)
157
self._lockmode = None
159
self._lockmode = mode
162
def _need_readlock(self):
163
if self._lockmode not in ['r', 'w']:
164
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
166
def _need_writelock(self):
167
if self._lockmode not in ['w']:
168
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
129
171
def abspath(self, name):
130
172
"""Return absolute filename for something in the branch"""
131
173
return os.path.join(self.base, name)
184
226
for d in ('text-store', 'inventory-store', 'revision-store'):
185
227
os.mkdir(self.controlfilename(d))
186
228
for f in ('revision-history', 'merged-patches',
187
'pending-merged-patches', 'branch-name'):
229
'pending-merged-patches', 'branch-name',
188
231
self.controlfile(f, 'w').write('')
189
232
mutter('created control directory in ' + self.base)
190
233
Inventory().write_xml(self.controlfile('inventory','w'))
226
270
That is to say, the inventory describing changes underway, that
227
271
will be committed to the next revision.
273
self._need_writelock()
229
274
## TODO: factor out to atomicfile? is rename safe on windows?
230
275
## TODO: Maybe some kind of clean/dirty marker on inventory?
231
276
tmpfname = self.controlfilename('inventory.tmp')
285
330
Traceback (most recent call last):
286
331
BzrError: ('cannot add: not a regular file or directory: nothere', [])
333
self._need_writelock()
289
335
# TODO: Re-adding a file that is removed in the working copy
290
336
# should probably put it back with the previous ID.
326
372
def print_file(self, file, revno):
327
373
"""Print `file` to stdout."""
374
self._need_readlock()
328
375
tree = self.revision_tree(self.lookup_revision(revno))
329
376
# use inventory as it was in that revision
330
377
file_id = tree.inventory.path2id(file)
607
656
def get_revision(self, revision_id):
608
657
"""Return the Revision object for a named revision"""
658
self._need_readlock()
609
659
r = Revision.read_xml(self.revision_store[revision_id])
610
660
assert r.revision_id == revision_id
617
667
TODO: Perhaps for this and similar methods, take a revision
618
668
parameter which can be either an integer revno or a
670
self._need_readlock()
620
671
i = Inventory.read_xml(self.inventory_store[inventory_id])
624
675
def get_revision_inventory(self, revision_id):
625
676
"""Return inventory of a past revision."""
677
self._need_readlock()
626
678
if revision_id == None:
627
679
return Inventory()
635
687
>>> ScratchBranch().revision_history()
690
self._need_readlock()
638
691
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
694
def enum_history(self, direction):
695
"""Return (revno, revision_id) for history of branch.
698
'forward' is from earliest to latest
699
'reverse' is from latest to earliest
701
rh = self.revision_history()
702
if direction == 'forward':
707
elif direction == 'reverse':
713
raise BzrError('invalid history direction %r' % direction)
642
717
"""Return current revision number for this branch.
723
def write_log(self, show_timezone='original', verbose=False):
724
"""Write out human-readable log of commits to this branch
726
utc -- If true, show dates in universal time, not local time."""
727
## TODO: Option to choose either original, utc or local timezone
730
for p in self.revision_history():
732
print 'revno:', revno
733
## TODO: Show hash if --id is given.
734
##print 'revision-hash:', p
735
rev = self.get_revision(p)
736
print 'committer:', rev.committer
737
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
740
## opportunistic consistency check, same as check_patch_chaining
741
if rev.precursor != precursor:
742
bailout("mismatched precursor!")
746
print ' (no message)'
748
for l in rev.message.split('\n'):
751
if verbose == True and precursor != None:
752
print 'changed files:'
753
tree = self.revision_tree(p)
754
prevtree = self.revision_tree(precursor)
756
for file_state, fid, old_name, new_name, kind in \
757
diff_trees(prevtree, tree, ):
758
if file_state == 'A' or file_state == 'M':
759
show_status(file_state, kind, new_name)
760
elif file_state == 'D':
761
show_status(file_state, kind, old_name)
762
elif file_state == 'R':
763
show_status(file_state, kind,
764
old_name + ' => ' + new_name)
770
798
def rename_one(self, from_rel, to_rel):
771
799
"""Rename one file.
773
801
This can change the directory or the filename or both.
803
self._need_writelock()
775
804
tree = self.working_tree()
776
805
inv = tree.inventory
777
806
if not tree.has_filename(from_rel):
826
855
Note that to_name is only the last component of the new name;
827
856
this doesn't change the directory.
858
self._need_writelock()
829
859
## TODO: Option to move IDs only
830
860
assert not isinstance(from_paths, basestring)
831
861
tree = self.working_tree()
958
989
def __del__(self):
959
993
"""Destroy the test branch, removing the scratch directory."""
995
mutter("delete ScratchBranch %s" % self.base)
961
996
shutil.rmtree(self.base)
963
998
# Work around for shutil.rmtree failing on Windows when
964
999
# readonly files are encountered
1000
mutter("hit exception in destroying ScratchBranch: %s" % e)
965
1001
for root, dirs, files in os.walk(self.base, topdown=False):
966
1002
for name in files:
967
1003
os.chmod(os.path.join(root, name), 0700)
968
1004
shutil.rmtree(self.base)