25
25
from inventory import Inventory
26
26
from trace import mutter, note
27
from tree import Tree, EmptyTree, RevisionTree
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, \
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
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 and (f.startswith('http://') or f.startswith('https://')):
46
return remotebranch.RemoteBranch(f, **args)
48
return Branch(f, **args)
51
43
def find_branch_root(f=None):
52
44
"""Find the branch root enclosing f, or pwd.
54
f may be a filename or a URL.
56
46
It is not necessary that f exists.
58
48
Basically we keep looking up until we find the control directory or
87
74
"""Branch holding a history of revisions.
90
Base directory of the branch.
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.
94
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
89
def __init__(self, base, init=False, find_root=True):
95
90
"""Create new branch object at a particular location.
97
92
base -- Base directory for the branch.
118
113
['use "bzr init" to initialize a new working tree',
119
114
'current bzr can only operate from top-of-tree'])
120
115
self._check_format()
123
117
self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
118
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
132
126
__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)
184
129
def abspath(self, name):
185
130
"""Return absolute filename for something in the branch"""
186
131
return os.path.join(self.base, name)
213
158
and binary. binary files are untranslated byte streams. Text
214
159
control files are stored with Unix newlines and in UTF-8, even
215
160
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.
221
163
fn = self.controlfilename(file_or_path)
223
165
if mode == 'rb' or mode == 'wb':
224
166
return file(fn, mode)
225
167
elif mode == 'r' or mode == 'w':
226
# open in binary mode anyhow so there's no newline translation;
227
# codecs uses line buffering by default; don't want that.
168
# open in binary mode anyhow so there's no newline translation
229
return codecs.open(fn, mode + 'b', 'utf-8',
170
return codecs.open(fn, mode + 'b', 'utf-8')
232
172
raise BzrError("invalid controlfile mode %r" % mode)
242
182
for d in ('text-store', 'inventory-store', 'revision-store'):
243
183
os.mkdir(self.controlfilename(d))
244
184
for f in ('revision-history', 'merged-patches',
245
'pending-merged-patches', 'branch-name',
185
'pending-merged-patches', 'branch-name'):
247
186
self.controlfile(f, 'w').write('')
248
187
mutter('created control directory in ' + self.base)
249
188
Inventory().write_xml(self.controlfile('inventory','w'))
286
224
That is to say, the inventory describing changes underway, that
287
225
will be committed to the next revision.
289
self._need_writelock()
290
227
## TODO: factor out to atomicfile? is rename safe on windows?
291
228
## TODO: Maybe some kind of clean/dirty marker on inventory?
292
229
tmpfname = self.controlfilename('inventory.tmp')
346
283
Traceback (most recent call last):
347
284
BzrError: ('cannot add: not a regular file or directory: nothere', [])
349
self._need_writelock()
351
287
# TODO: Re-adding a file that is removed in the working copy
352
288
# should probably put it back with the previous ID.
388
324
def print_file(self, file, revno):
389
325
"""Print `file` to stdout."""
390
self._need_readlock()
391
326
tree = self.revision_tree(self.lookup_revision(revno))
392
327
# use inventory as it was in that revision
393
328
file_id = tree.inventory.path2id(file)
672
605
def get_revision(self, revision_id):
673
606
"""Return the Revision object for a named revision"""
674
self._need_readlock()
675
607
r = Revision.read_xml(self.revision_store[revision_id])
676
608
assert r.revision_id == revision_id
683
615
TODO: Perhaps for this and similar methods, take a revision
684
616
parameter which can be either an integer revno or a
686
self._need_readlock()
687
618
i = Inventory.read_xml(self.inventory_store[inventory_id])
691
622
def get_revision_inventory(self, revision_id):
692
623
"""Return inventory of a past revision."""
693
self._need_readlock()
694
624
if revision_id == None:
695
625
return Inventory()
703
633
>>> ScratchBranch().revision_history()
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)
636
return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
721
def write_log(self, show_timezone='original', verbose=False):
722
"""Write out human-readable log of commits to this branch
724
utc -- If true, show dates in universal time, not local time."""
725
## TODO: Option to choose either original, utc or local timezone
728
for p in self.revision_history():
730
print 'revno:', revno
731
## TODO: Show hash if --id is given.
732
##print 'revision-hash:', p
733
rev = self.get_revision(p)
734
print 'committer:', rev.committer
735
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
738
## opportunistic consistency check, same as check_patch_chaining
739
if rev.precursor != precursor:
740
bailout("mismatched precursor!")
744
print ' (no message)'
746
for l in rev.message.split('\n'):
749
if verbose == True and precursor != None:
750
print 'changed files:'
751
tree = self.revision_tree(p)
752
prevtree = self.revision_tree(precursor)
754
for file_state, fid, old_name, new_name, kind in \
755
diff_trees(prevtree, tree, ):
756
if file_state == 'A' or file_state == 'M':
757
show_status(file_state, kind, new_name)
758
elif file_state == 'D':
759
show_status(file_state, kind, old_name)
760
elif file_state == 'R':
761
show_status(file_state, kind,
762
old_name + ' => ' + new_name)
815
768
def rename_one(self, from_rel, to_rel):
818
This can change the directory or the filename or both.
820
self._need_writelock()
821
769
tree = self.working_tree()
822
770
inv = tree.inventory
823
771
if not tree.has_filename(from_rel):
872
820
Note that to_name is only the last component of the new name;
873
821
this doesn't change the directory.
875
self._need_writelock()
876
823
## TODO: Option to move IDs only
877
824
assert not isinstance(from_paths, basestring)
878
825
tree = self.working_tree()
873
def show_status(self, show_all=False):
874
"""Display single-line status for non-ignored working files.
876
The list is show sorted in order by file name.
878
>>> b = ScratchBranch(files=['foo', 'foo~'])
884
>>> b.commit("add foo")
886
>>> os.unlink(b.abspath('foo'))
891
TODO: Get state for single files.
893
TODO: Perhaps show a slash at the end of directory names.
897
# We have to build everything into a list first so that it can
898
# sorted by name, incorporating all the different sources.
900
# FIXME: Rather than getting things in random order and then sorting,
901
# just step through in order.
903
# Interesting case: the old ID for a file has been removed,
904
# but a new file has been created under that name.
906
old = self.basis_tree()
907
new = self.working_tree()
909
for fs, fid, oldname, newname, kind in diff_trees(old, new):
911
show_status(fs, kind,
912
oldname + ' => ' + newname)
913
elif fs == 'A' or fs == 'M':
914
show_status(fs, kind, newname)
916
show_status(fs, kind, oldname)
919
show_status(fs, kind, newname)
922
show_status(fs, kind, newname)
924
show_status(fs, kind, newname)
926
bailout("weird file state %r" % ((fs, fid),))
927
930
class ScratchBranch(Branch):
928
931
"""Special test class: a branch that cleans up after itself.
953
956
def __del__(self):
957
957
"""Destroy the test branch, removing the scratch directory."""
959
mutter("delete ScratchBranch %s" % self.base)
960
959
shutil.rmtree(self.base)
962
961
# Work around for shutil.rmtree failing on Windows when
963
962
# readonly files are encountered
964
mutter("hit exception in destroying ScratchBranch: %s" % e)
965
963
for root, dirs, files in os.walk(self.base, topdown=False):
966
964
for name in files:
967
965
os.chmod(os.path.join(root, name), 0700)
968
966
shutil.rmtree(self.base)