26
26
from trace import mutter, note
27
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
28
from inventory import InventoryEntry, Inventory
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
32
32
from store import ImmutableStore
43
def find_branch(f, **args):
44
if f.startswith('http://') or f.startswith('https://'):
46
return remotebranch.RemoteBranch(f, **args)
48
return Branch(f, **args)
43
51
def find_branch_root(f=None):
44
52
"""Find the branch root enclosing f, or pwd.
54
f may be a filename or a URL.
46
56
It is not necessary that f exists.
48
58
Basically we keep looking up until we find the control directory or
74
87
"""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.
90
Base directory of the branch.
89
def __init__(self, base, init=False, find_root=True):
94
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
90
95
"""Create new branch object at a particular location.
92
97
base -- Base directory for the branch.
113
118
['use "bzr init" to initialize a new working tree',
114
119
'current bzr can only operate from top-of-tree'])
115
120
self._check_format()
117
123
self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
124
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
126
132
__repr__ = __str__
136
def lock(self, mode='w'):
137
"""Lock the on-disk branch, excluding other processes."""
143
om = os.O_WRONLY | os.O_CREAT
148
raise BzrError("invalid locking mode %r" % mode)
151
lockfile = os.open(self.controlfilename('branch-lock'), om)
153
if e.errno == errno.ENOENT:
154
# might not exist on branches from <0.0.4
155
self.controlfile('branch-lock', 'w').close()
156
lockfile = os.open(self.controlfilename('branch-lock'), om)
160
fcntl.lockf(lockfile, lm)
162
fcntl.lockf(lockfile, fcntl.LOCK_UN)
164
self._lockmode = None
166
self._lockmode = mode
168
warning("please write a locking method for platform %r" % sys.platform)
170
self._lockmode = None
172
self._lockmode = mode
175
def _need_readlock(self):
176
if self._lockmode not in ['r', 'w']:
177
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
179
def _need_writelock(self):
180
if self._lockmode not in ['w']:
181
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
129
184
def abspath(self, name):
130
185
"""Return absolute filename for something in the branch"""
131
186
return os.path.join(self.base, name)
158
213
and binary. binary files are untranslated byte streams. Text
159
214
control files are stored with Unix newlines and in UTF-8, even
160
215
if the platform or locale defaults are different.
217
Controlfiles should almost never be opened in write mode but
218
rather should be atomically copied and replaced using atomicfile.
163
221
fn = self.controlfilename(file_or_path)
184
242
for d in ('text-store', 'inventory-store', 'revision-store'):
185
243
os.mkdir(self.controlfilename(d))
186
244
for f in ('revision-history', 'merged-patches',
187
'pending-merged-patches', 'branch-name'):
245
'pending-merged-patches', 'branch-name',
188
247
self.controlfile(f, 'w').write('')
189
248
mutter('created control directory in ' + self.base)
190
249
Inventory().write_xml(self.controlfile('inventory','w'))
226
286
That is to say, the inventory describing changes underway, that
227
287
will be committed to the next revision.
289
self._need_writelock()
229
290
## TODO: factor out to atomicfile? is rename safe on windows?
230
291
## TODO: Maybe some kind of clean/dirty marker on inventory?
231
292
tmpfname = self.controlfilename('inventory.tmp')
285
346
Traceback (most recent call last):
286
347
BzrError: ('cannot add: not a regular file or directory: nothere', [])
349
self._need_writelock()
289
351
# TODO: Re-adding a file that is removed in the working copy
290
352
# should probably put it back with the previous ID.
326
388
def print_file(self, file, revno):
327
389
"""Print `file` to stdout."""
390
self._need_readlock()
328
391
tree = self.revision_tree(self.lookup_revision(revno))
329
392
# use inventory as it was in that revision
330
393
file_id = tree.inventory.path2id(file)
607
672
def get_revision(self, revision_id):
608
673
"""Return the Revision object for a named revision"""
674
self._need_readlock()
609
675
r = Revision.read_xml(self.revision_store[revision_id])
610
676
assert r.revision_id == revision_id
617
683
TODO: Perhaps for this and similar methods, take a revision
618
684
parameter which can be either an integer revno or a
686
self._need_readlock()
620
687
i = Inventory.read_xml(self.inventory_store[inventory_id])
624
691
def get_revision_inventory(self, revision_id):
625
692
"""Return inventory of a past revision."""
693
self._need_readlock()
626
694
if revision_id == None:
627
695
return Inventory()
635
703
>>> ScratchBranch().revision_history()
638
return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
706
self._need_readlock()
707
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
710
def enum_history(self, direction):
711
"""Return (revno, revision_id) for history of branch.
714
'forward' is from earliest to latest
715
'reverse' is from latest to earliest
717
rh = self.revision_history()
718
if direction == 'forward':
723
elif direction == 'reverse':
729
raise BzrError('invalid history direction %r' % direction)
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
814
def rename_one(self, from_rel, to_rel):
817
This can change the directory or the filename or both.
819
self._need_writelock()
771
820
tree = self.working_tree()
772
821
inv = tree.inventory
773
822
if not tree.has_filename(from_rel):
822
871
Note that to_name is only the last component of the new name;
823
872
this doesn't change the directory.
874
self._need_writelock()
825
875
## TODO: Option to move IDs only
826
876
assert not isinstance(from_paths, basestring)
827
877
tree = self.working_tree()
875
def show_status(self, show_all=False):
925
def show_status(self, show_all=False, file_list=None):
876
926
"""Display single-line status for non-ignored working files.
878
928
The list is show sorted in order by file name.
888
938
>>> os.unlink(b.abspath('foo'))
889
939
>>> b.show_status()
892
TODO: Get state for single files.
942
self._need_readlock()
895
944
# We have to build everything into a list first so that it can
896
945
# sorted by name, incorporating all the different sources.
904
953
old = self.basis_tree()
905
954
new = self.working_tree()
907
for fs, fid, oldname, newname, kind in diff_trees(old, new):
956
items = diff_trees(old, new)
957
# We want to filter out only if any file was provided in the file_list.
958
if isinstance(file_list, list) and len(file_list):
959
items = [item for item in items if item[3] in file_list]
961
for fs, fid, oldname, newname, kind in items:
909
963
show_status(fs, kind,
910
964
oldname + ' => ' + newname)
954
1008
def __del__(self):
955
1012
"""Destroy the test branch, removing the scratch directory."""
1014
mutter("delete ScratchBranch %s" % self.base)
957
1015
shutil.rmtree(self.base)
959
1017
# Work around for shutil.rmtree failing on Windows when
960
1018
# readonly files are encountered
1019
mutter("hit exception in destroying ScratchBranch: %s" % e)
961
1020
for root, dirs, files in os.walk(self.base, topdown=False):
962
1021
for name in files:
963
1022
os.chmod(os.path.join(root, name), 0700)
964
1023
shutil.rmtree(self.base)