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, \
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
74
74
"""Branch holding a history of revisions.
77
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.
87
:todo: mkdir() method.
81
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
89
def __init__(self, base, init=False, find_root=True):
82
90
"""Create new branch object at a particular location.
84
base -- Base directory for the branch.
92
:param base: Base directory for the branch.
86
init -- If True, create new control files in a previously
94
:param init: If True, create new control files in a previously
87
95
unversioned directory. If False, the branch must already
90
find_root -- If true and init is false, find the root of the
98
:param find_root: If true and init is false, find the root of the
91
99
existing branch containing base.
93
101
In the test suite, creation of new trees is tested using the
105
113
['use "bzr init" to initialize a new working tree',
106
114
'current bzr can only operate from top-of-tree'])
107
115
self._check_format()
110
117
self.text_store = ImmutableStore(self.controlfilename('text-store'))
111
118
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
119
126
__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)
171
129
def abspath(self, name):
172
130
"""Return absolute filename for something in the branch"""
173
131
return os.path.join(self.base, name)
196
154
def controlfile(self, file_or_path, mode='r'):
197
"""Open a control file for this branch.
199
There are two classes of file in the control directory: text
200
and binary. binary files are untranslated byte streams. Text
201
control files are stored with Unix newlines and in UTF-8, even
202
if the platform or locale defaults are different.
205
fn = self.controlfilename(file_or_path)
207
if mode == 'rb' or mode == 'wb':
208
return file(fn, mode)
209
elif mode == 'r' or mode == 'w':
210
# open in binary mode anyhow so there's no newline translation;
211
# codecs uses line buffering by default; don't want that.
213
return codecs.open(fn, mode + 'b', 'utf-8',
216
raise BzrError("invalid controlfile mode %r" % mode)
155
"""Open a control file for this branch"""
156
return file(self.controlfilename(file_or_path), mode)
220
159
def _make_control(self):
222
161
self.controlfile('README', 'w').write(
223
162
"This is a Bazaar-NG control directory.\n"
224
163
"Do not change any files in this directory.")
225
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
164
self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT)
226
165
for d in ('text-store', 'inventory-store', 'revision-store'):
227
166
os.mkdir(self.controlfilename(d))
228
167
for f in ('revision-history', 'merged-patches',
229
'pending-merged-patches', 'branch-name',
168
'pending-merged-patches', 'branch-name'):
231
169
self.controlfile(f, 'w').write('')
232
170
mutter('created control directory in ' + self.base)
233
171
Inventory().write_xml(self.controlfile('inventory','w'))
244
182
# This ignores newlines so that we can open branches created
245
183
# on Windows from Linux and so on. I think it might be better
246
184
# to always make all internal files in unix format.
247
fmt = self.controlfile('branch-format', 'r').read()
185
fmt = self.controlfile('branch-format', 'rb').read()
248
186
fmt.replace('\r\n', '')
249
187
if fmt != BZR_BRANCH_FORMAT:
250
188
bailout('sorry, branch format %r not supported' % fmt,
255
193
def read_working_inventory(self):
256
194
"""Read the working inventory."""
257
self._need_readlock()
258
195
before = time.time()
259
# ElementTree does its own conversion from UTF-8, so open in
261
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
196
inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
262
197
mutter("loaded inventory of %d items in %f"
263
198
% (len(inv), time.time() - before))
270
205
That is to say, the inventory describing changes underway, that
271
206
will be committed to the next revision.
273
self._need_writelock()
274
208
## TODO: factor out to atomicfile? is rename safe on windows?
275
209
## TODO: Maybe some kind of clean/dirty marker on inventory?
276
210
tmpfname = self.controlfilename('inventory.tmp')
277
tmpf = file(tmpfname, 'wb')
211
tmpf = file(tmpfname, 'w')
278
212
inv.write_xml(tmpf)
280
214
inv_fname = self.controlfilename('inventory')
291
225
def add(self, files, verbose=False):
292
226
"""Make files versioned.
294
Note that the command line normally calls smart_add instead.
296
228
This puts the files in the Added state, so that they will be
297
229
recorded by the next commit.
299
TODO: Perhaps have an option to add the ids even if the files do
231
:todo: Perhaps have an option to add the ids even if the files do
302
TODO: Perhaps return the ids of the files? But then again it
234
:todo: Perhaps return the ids of the files? But then again it
303
235
is easy to retrieve them if they're needed.
305
TODO: Option to specify file id.
237
:todo: Option to specify file id.
307
TODO: Adding a directory should optionally recurse down and
239
:todo: Adding a directory should optionally recurse down and
308
240
add all non-ignored children. Perhaps do that in a
309
241
higher-level method.
330
262
Traceback (most recent call last):
331
263
BzrError: ('cannot add: not a regular file or directory: nothere', [])
333
self._need_writelock()
335
266
# TODO: Re-adding a file that is removed in the working copy
336
267
# should probably put it back with the previous ID.
372
303
def print_file(self, file, revno):
373
304
"""Print `file` to stdout."""
374
self._need_readlock()
375
305
tree = self.revision_tree(self.lookup_revision(revno))
376
306
# use inventory as it was in that revision
377
307
file_id = tree.inventory.path2id(file)
386
316
This does not remove their text. This does not run on
388
TODO: Refuse to remove modified files unless --force is given?
318
:todo: Refuse to remove modified files unless --force is given?
390
320
>>> b = ScratchBranch(files=['foo'])
409
339
>>> b.working_tree().has_filename('foo')
412
TODO: Do something useful with directories.
342
:todo: Do something useful with directories.
414
TODO: Should this remove the text or not? Tough call; not
344
:todo: Should this remove the text or not? Tough call; not
415
345
removing may be useful and the user can just use use rm, and
416
346
is the opposite of add. Removing it is consistent with most
417
347
other tools. Maybe an option.
419
349
## TODO: Normalize names
420
350
## TODO: Remove nested loops; better scalability
421
self._need_writelock()
423
352
if isinstance(files, types.StringTypes):
482
411
be robust against files disappearing, moving, etc. So the
483
412
whole thing is a bit hard.
485
timestamp -- if not None, seconds-since-epoch for a
414
:param timestamp: if not None, seconds-since-epoch for a
486
415
postdated/predated commit.
488
self._need_writelock()
490
418
## TODO: Show branch names
627
555
## TODO: Also calculate and store the inventory SHA1
628
556
mutter("committing patch r%d" % (self.revno() + 1))
558
mutter("append to revision-history")
559
f = self.controlfile('revision-history', 'at')
560
f.write(rev_id + '\n')
631
self.append_revision(rev_id)
634
564
note("commited r%d" % self.revno())
637
def append_revision(self, revision_id):
638
mutter("add {%s} to revision-history" % revision_id)
639
rev_history = self.revision_history()
641
tmprhname = self.controlfilename('revision-history.tmp')
642
rhname = self.controlfilename('revision-history')
644
f = file(tmprhname, 'wt')
645
rev_history.append(revision_id)
646
f.write('\n'.join(rev_history))
650
if sys.platform == 'win32':
652
os.rename(tmprhname, rhname)
656
567
def get_revision(self, revision_id):
657
568
"""Return the Revision object for a named revision"""
658
self._need_readlock()
659
569
r = Revision.read_xml(self.revision_store[revision_id])
660
570
assert r.revision_id == revision_id
664
574
def get_inventory(self, inventory_id):
665
575
"""Get Inventory object by hash.
667
TODO: Perhaps for this and similar methods, take a revision
577
:todo: Perhaps for this and similar methods, take a revision
668
578
parameter which can be either an integer revno or a
670
self._need_readlock()
671
580
i = Inventory.read_xml(self.inventory_store[inventory_id])
675
584
def get_revision_inventory(self, revision_id):
676
585
"""Return inventory of a past revision."""
677
self._need_readlock()
678
586
if revision_id == None:
679
587
return Inventory()
687
595
>>> ScratchBranch().revision_history()
690
self._need_readlock()
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)
598
return [chomp(l) for l in self.controlfile('revision-history').readlines()]
683
def write_log(self, show_timezone='original'):
684
"""Write out human-readable log of commits to this branch
686
:param utc: If true, show dates in universal time, not local time."""
687
## TODO: Option to choose either original, utc or local timezone
690
for p in self.revision_history():
692
print 'revno:', revno
693
## TODO: Show hash if --id is given.
694
##print 'revision-hash:', p
695
rev = self.get_revision(p)
696
print 'committer:', rev.committer
697
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
700
## opportunistic consistency check, same as check_patch_chaining
701
if rev.precursor != precursor:
702
bailout("mismatched precursor!")
706
print ' (no message)'
708
for l in rev.message.split('\n'):
798
715
def rename_one(self, from_rel, to_rel):
801
This can change the directory or the filename or both.
803
self._need_writelock()
804
716
tree = self.working_tree()
805
717
inv = tree.inventory
806
718
if not tree.has_filename(from_rel):
855
767
Note that to_name is only the last component of the new name;
856
768
this doesn't change the directory.
858
self._need_writelock()
859
770
## TODO: Option to move IDs only
860
771
assert not isinstance(from_paths, basestring)
861
772
tree = self.working_tree()
923
834
>>> b.show_status()
926
TODO: Get state for single files.
838
:todo: Get state for single files.
840
:todo: Perhaps show a slash at the end of directory names.
928
self._need_readlock()
930
844
# We have to build everything into a list first so that it can
931
845
# sorted by name, incorporating all the different sources.
957
871
show_status(fs, kind, newname)
959
bailout("weird file state %r" % ((fs, fid),))
873
bailout("wierd file state %r" % ((fs, fid),))
989
903
def __del__(self):
993
904
"""Destroy the test branch, removing the scratch directory."""
995
mutter("delete ScratchBranch %s" % self.base)
996
906
shutil.rmtree(self.base)
998
908
# Work around for shutil.rmtree failing on Windows when
999
909
# readonly files are encountered
1000
mutter("hit exception in destroying ScratchBranch: %s" % e)
1001
910
for root, dirs, files in os.walk(self.base, topdown=False):
1002
911
for name in files:
1003
912
os.chmod(os.path.join(root, name), 0700)
1004
913
shutil.rmtree(self.base)