23
25
from inventory import Inventory
24
26
from trace import mutter, note
25
from tree import Tree, EmptyTree, RevisionTree
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
26
28
from inventory import InventoryEntry, Inventory
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
28
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
30
32
from store import ImmutableStore
31
33
from revision import Revision
32
from errors import BzrError
34
from errors import bailout, BzrError
33
35
from textui import show_status
36
from diff import diff_trees
35
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
39
## TODO: Maybe include checks for common corruption of newlines, etc?
40
def find_branch(f, **args):
41
if f and (f.startswith('http://') or f.startswith('https://')):
43
return remotebranch.RemoteBranch(f, **args)
45
return Branch(f, **args)
49
def with_writelock(method):
50
"""Method decorator for functions run with the branch locked."""
52
# called with self set to the branch
55
return method(self, *a, **k)
61
def with_readlock(method):
65
return method(self, *a, **k)
71
43
def find_branch_root(f=None):
72
44
"""Find the branch root enclosing f, or pwd.
74
f may be a filename or a URL.
76
46
It is not necessary that f exists.
78
48
Basically we keep looking up until we find the control directory or
103
70
######################################################################
106
class Branch(object):
107
74
"""Branch holding a history of revisions.
110
Base directory of the branch.
116
If _lock_mode is true, a positive count of the number of times the
120
Open file used for locking.
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.
126
89
def __init__(self, base, init=False, find_root=True):
127
90
"""Create new branch object at a particular location.
129
base -- Base directory for the branch.
92
:param base: Base directory for the branch.
131
init -- If True, create new control files in a previously
94
:param init: If True, create new control files in a previously
132
95
unversioned directory. If False, the branch must already
135
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
136
99
existing branch containing base.
138
101
In the test suite, creation of new trees is tested using the
235
154
def controlfile(self, file_or_path, mode='r'):
236
"""Open a control file for this branch.
238
There are two classes of file in the control directory: text
239
and binary. binary files are untranslated byte streams. Text
240
control files are stored with Unix newlines and in UTF-8, even
241
if the platform or locale defaults are different.
243
Controlfiles should almost never be opened in write mode but
244
rather should be atomically copied and replaced using atomicfile.
247
fn = self.controlfilename(file_or_path)
249
if mode == 'rb' or mode == 'wb':
250
return file(fn, mode)
251
elif mode == 'r' or mode == 'w':
252
# open in binary mode anyhow so there's no newline translation;
253
# codecs uses line buffering by default; don't want that.
255
return codecs.open(fn, mode + 'b', 'utf-8',
258
raise BzrError("invalid controlfile mode %r" % mode)
155
"""Open a control file for this branch"""
156
return file(self.controlfilename(file_or_path), mode)
262
159
def _make_control(self):
286
182
# This ignores newlines so that we can open branches created
287
183
# on Windows from Linux and so on. I think it might be better
288
184
# to always make all internal files in unix format.
289
fmt = self.controlfile('branch-format', 'r').read()
185
fmt = self.controlfile('branch-format', 'rb').read()
290
186
fmt.replace('\r\n', '')
291
187
if fmt != BZR_BRANCH_FORMAT:
292
raise BzrError('sorry, branch format %r not supported' % fmt,
293
['use a different bzr version',
294
'or remove the .bzr directory and "bzr init" again'])
188
bailout('sorry, branch format %r not supported' % fmt,
189
['use a different bzr version',
190
'or remove the .bzr directory and "bzr init" again'])
299
193
def read_working_inventory(self):
300
194
"""Read the working inventory."""
301
195
before = time.time()
302
# ElementTree does its own conversion from UTF-8, so open in
304
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
196
inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
305
197
mutter("loaded inventory of %d items in %f"
306
198
% (len(inv), time.time() - before))
310
202
def _write_inventory(self, inv):
311
203
"""Update the working inventory.
324
216
os.remove(inv_fname)
325
217
os.rename(tmpfname, inv_fname)
326
218
mutter('wrote working inventory')
329
221
inventory = property(read_working_inventory, _write_inventory, None,
330
222
"""Inventory for the working copy.""")
334
def add(self, files, verbose=False, ids=None):
225
def add(self, files, verbose=False):
335
226
"""Make files versioned.
337
Note that the command line normally calls smart_add instead.
339
228
This puts the files in the Added state, so that they will be
340
229
recorded by the next commit.
342
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
345
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
346
235
is easy to retrieve them if they're needed.
348
TODO: Option to specify file id.
237
:todo: Option to specify file id.
350
TODO: Adding a directory should optionally recurse down and
239
:todo: Adding a directory should optionally recurse down and
351
240
add all non-ignored children. Perhaps do that in a
352
241
higher-level method.
243
>>> b = ScratchBranch(files=['foo'])
244
>>> 'foo' in b.unknowns()
249
>>> 'foo' in b.unknowns()
251
>>> bool(b.inventory.path2id('foo'))
257
Traceback (most recent call last):
259
BzrError: ('foo is already versioned', [])
261
>>> b.add(['nothere'])
262
Traceback (most recent call last):
263
BzrError: ('cannot add: not a regular file or directory: nothere', [])
354
266
# TODO: Re-adding a file that is removed in the working copy
355
267
# should probably put it back with the previous ID.
356
268
if isinstance(files, types.StringTypes):
357
assert(ids is None or isinstance(ids, types.StringTypes))
363
ids = [None] * len(files)
365
assert(len(ids) == len(files))
367
271
inv = self.read_working_inventory()
368
for f,file_id in zip(files, ids):
369
273
if is_control_file(f):
370
raise BzrError("cannot add control file %s" % quotefn(f))
274
bailout("cannot add control file %s" % quotefn(f))
372
276
fp = splitpath(f)
375
raise BzrError("cannot add top-level %r" % f)
279
bailout("cannot add top-level %r" % f)
377
281
fullpath = os.path.normpath(self.abspath(f))
380
284
kind = file_kind(fullpath)
382
286
# maybe something better?
383
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
287
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
385
289
if kind != 'file' and kind != 'directory':
386
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
290
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
389
file_id = gen_file_id(f)
292
file_id = gen_file_id(f)
390
293
inv.add_path(f, kind=kind, file_id=file_id)
393
296
show_status('A', kind, quotefn(f))
395
298
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
397
300
self._write_inventory(inv)
400
303
def print_file(self, file, revno):
401
304
"""Print `file` to stdout."""
403
306
# use inventory as it was in that revision
404
307
file_id = tree.inventory.path2id(file)
406
raise BzrError("%r is not present in revision %d" % (file, revno))
309
bailout("%r is not present in revision %d" % (file, revno))
407
310
tree.print_file(file_id)
411
313
def remove(self, files, verbose=False):
412
314
"""Mark nominated files for removal from the inventory.
414
316
This does not remove their text. This does not run on
416
TODO: Refuse to remove modified files unless --force is given?
418
TODO: Do something useful with directories.
420
TODO: Should this remove the text or not? Tough call; not
318
:todo: Refuse to remove modified files unless --force is given?
320
>>> b = ScratchBranch(files=['foo'])
322
>>> b.inventory.has_filename('foo')
325
>>> b.working_tree().has_filename('foo')
327
>>> b.inventory.has_filename('foo')
330
>>> b = ScratchBranch(files=['foo'])
335
>>> b.inventory.has_filename('foo')
337
>>> b.basis_tree().has_filename('foo')
339
>>> b.working_tree().has_filename('foo')
342
:todo: Do something useful with directories.
344
:todo: Should this remove the text or not? Tough call; not
421
345
removing may be useful and the user can just use use rm, and
422
346
is the opposite of add. Removing it is consistent with most
423
347
other tools. Maybe an option.
425
349
## TODO: Normalize names
426
350
## TODO: Remove nested loops; better scalability
427
352
if isinstance(files, types.StringTypes):
430
355
tree = self.working_tree()
431
356
inv = tree.inventory
477
392
return self.working_tree().unknowns()
480
def append_revision(self, revision_id):
481
mutter("add {%s} to revision-history" % revision_id)
482
rev_history = self.revision_history()
484
tmprhname = self.controlfilename('revision-history.tmp')
485
rhname = self.controlfilename('revision-history')
487
f = file(tmprhname, 'wt')
488
rev_history.append(revision_id)
489
f.write('\n'.join(rev_history))
395
def commit(self, message, timestamp=None, timezone=None,
398
"""Commit working copy as a new revision.
400
The basic approach is to add all the file texts into the
401
store, then the inventory, then make a new revision pointing
402
to that inventory and store that.
404
This is not quite safe if the working copy changes during the
405
commit; for the moment that is simply not allowed. A better
406
approach is to make a temporary copy of the files before
407
computing their hashes, and then add those hashes in turn to
408
the inventory. This should mean at least that there are no
409
broken hash pointers. There is no way we can get a snapshot
410
of the whole directory at an instant. This would also have to
411
be robust against files disappearing, moving, etc. So the
412
whole thing is a bit hard.
414
:param timestamp: if not None, seconds-since-epoch for a
415
postdated/predated commit.
418
## TODO: Show branch names
420
# TODO: Don't commit if there are no changes, unless forced?
422
# First walk over the working inventory; and both update that
423
# and also build a new revision inventory. The revision
424
# inventory needs to hold the text-id, sha1 and size of the
425
# actual file versions committed in the revision. (These are
426
# not present in the working inventory.) We also need to
427
# detect missing/deleted files, and remove them from the
430
work_inv = self.read_working_inventory()
432
basis = self.basis_tree()
433
basis_inv = basis.inventory
435
for path, entry in work_inv.iter_entries():
436
## TODO: Cope with files that have gone missing.
438
## TODO: Check that the file kind has not changed from the previous
439
## revision of this file (if any).
443
p = self.abspath(path)
444
file_id = entry.file_id
445
mutter('commit prep file %s, id %r ' % (p, file_id))
447
if not os.path.exists(p):
448
mutter(" file is missing, removing from inventory")
450
show_status('D', entry.kind, quotefn(path))
451
missing_ids.append(file_id)
454
# TODO: Handle files that have been deleted
456
# TODO: Maybe a special case for empty files? Seems a
457
# waste to store them many times.
461
if basis_inv.has_id(file_id):
462
old_kind = basis_inv[file_id].kind
463
if old_kind != entry.kind:
464
bailout("entry %r changed kind from %r to %r"
465
% (file_id, old_kind, entry.kind))
467
if entry.kind == 'directory':
469
bailout("%s is entered as directory but not a directory" % quotefn(p))
470
elif entry.kind == 'file':
472
bailout("%s is entered as file but is not a file" % quotefn(p))
474
content = file(p, 'rb').read()
476
entry.text_sha1 = sha_string(content)
477
entry.text_size = len(content)
479
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
481
and (old_ie.text_size == entry.text_size)
482
and (old_ie.text_sha1 == entry.text_sha1)):
483
## assert content == basis.get_file(file_id).read()
484
entry.text_id = basis_inv[file_id].text_id
485
mutter(' unchanged from previous text_id {%s}' %
489
entry.text_id = gen_file_id(entry.name)
490
self.text_store.add(content, entry.text_id)
491
mutter(' stored with text_id {%s}' % entry.text_id)
495
elif (old_ie.name == entry.name
496
and old_ie.parent_id == entry.parent_id):
501
show_status(state, entry.kind, quotefn(path))
503
for file_id in missing_ids:
504
# have to do this later so we don't mess up the iterator.
505
# since parents may be removed before their children we
508
# FIXME: There's probably a better way to do this; perhaps
509
# the workingtree should know how to filter itself.
510
if work_inv.has_id(file_id):
511
del work_inv[file_id]
514
inv_id = rev_id = _gen_revision_id(time.time())
516
inv_tmp = tempfile.TemporaryFile()
517
inv.write_xml(inv_tmp)
519
self.inventory_store.add(inv_tmp, inv_id)
520
mutter('new inventory_id is {%s}' % inv_id)
522
self._write_inventory(work_inv)
524
if timestamp == None:
525
timestamp = time.time()
527
if committer == None:
528
committer = username()
531
timezone = local_time_offset()
533
mutter("building commit log message")
534
rev = Revision(timestamp=timestamp,
537
precursor = self.last_patch(),
542
rev_tmp = tempfile.TemporaryFile()
543
rev.write_xml(rev_tmp)
545
self.revision_store.add(rev_tmp, rev_id)
546
mutter("new revision_id is {%s}" % rev_id)
548
## XXX: Everything up to here can simply be orphaned if we abort
549
## the commit; it will leave junk files behind but that doesn't
552
## TODO: Read back the just-generated changeset, and make sure it
553
## applies and recreates the right state.
555
## TODO: Also calculate and store the inventory SHA1
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')
493
if sys.platform == 'win32':
495
os.rename(tmprhname, rhname)
564
note("commited r%d" % self.revno())
499
567
def get_revision(self, revision_id):
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'):
627
715
def rename_one(self, from_rel, to_rel):
630
This can change the directory or the filename or both.
632
716
tree = self.working_tree()
633
717
inv = tree.inventory
634
718
if not tree.has_filename(from_rel):
635
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
719
bailout("can't rename: old working file %r does not exist" % from_rel)
636
720
if tree.has_filename(to_rel):
637
raise BzrError("can't rename: new working file %r already exists" % to_rel)
721
bailout("can't rename: new working file %r already exists" % to_rel)
639
723
file_id = inv.path2id(from_rel)
640
724
if file_id == None:
641
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
725
bailout("can't rename: old name %r is not versioned" % from_rel)
643
727
if inv.path2id(to_rel):
644
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
728
bailout("can't rename: new name %r is already versioned" % to_rel)
646
730
to_dir, to_tail = os.path.split(to_rel)
647
731
to_dir_id = inv.path2id(to_dir)
648
732
if to_dir_id == None and to_dir != '':
649
raise BzrError("can't determine destination directory id for %r" % to_dir)
733
bailout("can't determine destination directory id for %r" % to_dir)
651
735
mutter("rename_one:")
652
736
mutter(" file_id {%s}" % file_id)
690
773
inv = tree.inventory
691
774
to_abs = self.abspath(to_name)
692
775
if not isdir(to_abs):
693
raise BzrError("destination %r is not a directory" % to_abs)
776
bailout("destination %r is not a directory" % to_abs)
694
777
if not tree.has_filename(to_name):
695
raise BzrError("destination %r not in working directory" % to_abs)
778
bailout("destination %r not in working directory" % to_abs)
696
779
to_dir_id = inv.path2id(to_name)
697
780
if to_dir_id == None and to_name != '':
698
raise BzrError("destination %r is not a versioned directory" % to_name)
781
bailout("destination %r is not a versioned directory" % to_name)
699
782
to_dir_ie = inv[to_dir_id]
700
783
if to_dir_ie.kind not in ('directory', 'root_directory'):
701
raise BzrError("destination %r is not a directory" % to_abs)
784
bailout("destination %r is not a directory" % to_abs)
703
to_idpath = inv.get_idpath(to_dir_id)
786
to_idpath = Set(inv.get_idpath(to_dir_id))
705
788
for f in from_paths:
706
789
if not tree.has_filename(f):
707
raise BzrError("%r does not exist in working tree" % f)
790
bailout("%r does not exist in working tree" % f)
708
791
f_id = inv.path2id(f)
710
raise BzrError("%r is not versioned" % f)
793
bailout("%r is not versioned" % f)
711
794
name_tail = splitpath(f)[-1]
712
795
dest_path = appendpath(to_name, name_tail)
713
796
if tree.has_filename(dest_path):
714
raise BzrError("destination %r already exists" % dest_path)
797
bailout("destination %r already exists" % dest_path)
715
798
if f_id in to_idpath:
716
raise BzrError("can't move %r to a subdirectory of itself" % f)
799
bailout("can't move %r to a subdirectory of itself" % f)
718
801
# OK, so there's a race here, it's possible that someone will
719
802
# create a file in this interval and then the rename might be
728
811
os.rename(self.abspath(f), self.abspath(dest_path))
729
812
except OSError, e:
730
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
813
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
731
814
["rename rolled back"])
733
816
self._write_inventory(inv)
820
def show_status(self, show_all=False):
821
"""Display single-line status for non-ignored working files.
823
The list is show sorted in order by file name.
825
>>> b = ScratchBranch(files=['foo', 'foo~'])
831
>>> b.commit("add foo")
833
>>> os.unlink(b.abspath('foo'))
838
:todo: Get state for single files.
840
:todo: Perhaps show a slash at the end of directory names.
844
# We have to build everything into a list first so that it can
845
# sorted by name, incorporating all the different sources.
847
# FIXME: Rather than getting things in random order and then sorting,
848
# just step through in order.
850
# Interesting case: the old ID for a file has been removed,
851
# but a new file has been created under that name.
853
old = self.basis_tree()
854
new = self.working_tree()
856
for fs, fid, oldname, newname, kind in diff_trees(old, new):
858
show_status(fs, kind,
859
oldname + ' => ' + newname)
860
elif fs == 'A' or fs == 'M':
861
show_status(fs, kind, newname)
863
show_status(fs, kind, oldname)
866
show_status(fs, kind, newname)
869
show_status(fs, kind, newname)
871
show_status(fs, kind, newname)
873
bailout("wierd file state %r" % ((fs, fid),))
738
877
class ScratchBranch(Branch):
739
878
"""Special test class: a branch that cleans up after itself.