1
# This program is free software; you can redistribute it and/or modify
2
# it under the terms of the GNU General Public License as published by
3
# the Free Software Foundation; either version 2 of the License, or
4
# (at your option) any later version.
6
# This program is distributed in the hope that it will be useful,
7
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9
# GNU General Public License for more details.
11
# You should have received a copy of the GNU General Public License
12
# along with this program; if not, write to the Free Software
13
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
import traceback, socket, fnmatch, difflib, time
20
from binascii import hexlify
23
from inventory import Inventory
24
from trace import mutter, note
25
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
26
from inventory import InventoryEntry, Inventory
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
28
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
joinpath, sha_string, file_kind, local_time_offset
30
from store import ImmutableStore
31
from revision import Revision
32
from errors import bailout
33
from textui import show_status
34
from diff import diff_trees
36
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
37
## TODO: Maybe include checks for common corruption of newlines, etc?
41
def find_branch_root(f=None):
42
"""Find the branch root enclosing f, or pwd.
44
It is not necessary that f exists.
46
Basically we keep looking up until we find the control directory or
50
elif hasattr(os.path, 'realpath'):
51
f = os.path.realpath(f)
53
f = os.path.abspath(f)
59
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
61
head, tail = os.path.split(f)
63
# reached the root, whatever that may be
64
bailout('%r is not in a branch' % orig_f)
69
######################################################################
73
"""Branch holding a history of revisions.
75
:todo: Perhaps use different stores for different classes of object,
76
so that we can keep track of how much space each one uses,
77
or garbage-collect them.
79
:todo: Add a RemoteBranch subclass. For the basic case of read-only
80
HTTP access this should be very easy by,
81
just redirecting controlfile access into HTTP requests.
82
We would need a RemoteStore working similarly.
84
:todo: Keep the on-disk branch locked while the object exists.
86
:todo: mkdir() method.
88
def __init__(self, base, init=False, find_root=True):
89
"""Create new branch object at a particular location.
91
:param base: Base directory for the branch.
93
:param init: If True, create new control files in a previously
94
unversioned directory. If False, the branch must already
97
:param find_root: If true and init is false, find the root of the
98
existing branch containing base.
100
In the test suite, creation of new trees is tested using the
101
`ScratchBranch` class.
104
self.base = os.path.realpath(base)
107
self.base = find_branch_root(base)
109
self.base = os.path.realpath(base)
110
if not isdir(self.controlfilename('.')):
111
bailout("not a bzr branch: %s" % quotefn(base),
112
['use "bzr init" to initialize a new working tree',
113
'current bzr can only operate from top-of-tree'])
116
self.text_store = ImmutableStore(self.controlfilename('text-store'))
117
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
118
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
122
return '%s(%r)' % (self.__class__.__name__, self.base)
128
def abspath(self, name):
129
"""Return absolute filename for something in the branch"""
130
return os.path.join(self.base, name)
133
def controlfilename(self, file_or_path):
134
"""Return location relative to branch."""
135
if isinstance(file_or_path, types.StringTypes):
136
file_or_path = [file_or_path]
137
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
140
def controlfile(self, file_or_path, mode='r'):
141
"""Open a control file for this branch"""
142
return file(self.controlfilename(file_or_path), mode)
145
def _make_control(self):
146
os.mkdir(self.controlfilename([]))
147
self.controlfile('README', 'w').write(
148
"This is a Bazaar-NG control directory.\n"
149
"Do not change any files in this directory.")
150
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
151
for d in ('text-store', 'inventory-store', 'revision-store'):
152
os.mkdir(self.controlfilename(d))
153
for f in ('revision-history', 'merged-patches',
154
'pending-merged-patches', 'branch-name'):
155
self.controlfile(f, 'w').write('')
156
mutter('created control directory in ' + self.base)
157
Inventory().write_xml(self.controlfile('inventory','w'))
160
def _check_format(self):
161
"""Check this branch format is supported.
163
The current tool only supports the current unstable format.
165
In the future, we might need different in-memory Branch
166
classes to support downlevel branches. But not yet.
168
# read in binary mode to detect newline wierdness.
169
fmt = self.controlfile('branch-format', 'rb').read()
170
if fmt != BZR_BRANCH_FORMAT:
171
bailout('sorry, branch format %r not supported' % fmt,
172
['use a different bzr version',
173
'or remove the .bzr directory and "bzr init" again'])
176
def read_working_inventory(self):
177
"""Read the working inventory."""
179
inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
180
mutter("loaded inventory of %d items in %f"
181
% (len(inv), time.time() - before))
185
def _write_inventory(self, inv):
186
"""Update the working inventory.
188
That is to say, the inventory describing changes underway, that
189
will be committed to the next revision.
191
## TODO: factor out to atomicfile? is rename safe on windows?
192
tmpfname = self.controlfilename('inventory.tmp')
193
tmpf = file(tmpfname, 'w')
196
os.rename(tmpfname, self.controlfilename('inventory'))
197
mutter('wrote working inventory')
200
inventory = property(read_working_inventory, _write_inventory, None,
201
"""Inventory for the working copy.""")
204
def add(self, files, verbose=False):
205
"""Make files versioned.
207
This puts the files in the Added state, so that they will be
208
recorded by the next commit.
210
:todo: Perhaps have an option to add the ids even if the files do
213
:todo: Perhaps return the ids of the files? But then again it
214
is easy to retrieve them if they're needed.
216
:todo: Option to specify file id.
218
:todo: Adding a directory should optionally recurse down and
219
add all non-ignored children. Perhaps do that in a
222
>>> b = ScratchBranch(files=['foo'])
223
>>> 'foo' in b.unknowns()
228
>>> 'foo' in b.unknowns()
230
>>> bool(b.inventory.path2id('foo'))
236
Traceback (most recent call last):
238
BzrError: ('foo is already versioned', [])
240
>>> b.add(['nothere'])
241
Traceback (most recent call last):
242
BzrError: ('cannot add: not a regular file or directory: nothere', [])
245
# TODO: Re-adding a file that is removed in the working copy
246
# should probably put it back with the previous ID.
247
if isinstance(files, types.StringTypes):
250
inv = self.read_working_inventory()
252
if is_control_file(f):
253
bailout("cannot add control file %s" % quotefn(f))
258
bailout("cannot add top-level %r" % f)
260
fullpath = os.path.normpath(self.abspath(f))
264
elif isdir(fullpath):
267
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
270
parent_name = joinpath(fp[:-1])
271
mutter("lookup parent %r" % parent_name)
272
parent_id = inv.path2id(parent_name)
273
if parent_id == None:
274
bailout("cannot add: parent %r is not versioned"
279
file_id = _gen_file_id(fp[-1])
280
inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
282
show_status('A', kind, quotefn(f))
284
mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
285
% (f, file_id, kind, parent_id))
286
self._write_inventory(inv)
290
def remove(self, files, verbose=False):
291
"""Mark nominated files for removal from the inventory.
293
This does not remove their text. This does not run on
295
:todo: Refuse to remove modified files unless --force is given?
297
>>> b = ScratchBranch(files=['foo'])
299
>>> b.inventory.has_filename('foo')
302
>>> b.working_tree().has_filename('foo')
304
>>> b.inventory.has_filename('foo')
307
>>> b = ScratchBranch(files=['foo'])
312
>>> b.inventory.has_filename('foo')
314
>>> b.basis_tree().has_filename('foo')
316
>>> b.working_tree().has_filename('foo')
319
:todo: Do something useful with directories.
321
:todo: Should this remove the text or not? Tough call; not
322
removing may be useful and the user can just use use rm, and
323
is the opposite of add. Removing it is consistent with most
324
other tools. Maybe an option.
326
## TODO: Normalize names
327
## TODO: Remove nested loops; better scalability
329
if isinstance(files, types.StringTypes):
332
tree = self.working_tree()
335
# do this before any modifications
339
bailout("cannot remove unversioned file %s" % quotefn(f))
340
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
342
# having remove it, it must be either ignored or unknown
343
if tree.is_ignored(f):
347
show_status(new_status, inv[fid].kind, quotefn(f))
350
self._write_inventory(inv)
354
"""Return all unknown files.
356
These are files in the working directory that are not versioned or
357
control files or ignored.
359
>>> b = ScratchBranch(files=['foo', 'foo~'])
360
>>> list(b.unknowns())
363
>>> list(b.unknowns())
366
>>> list(b.unknowns())
369
return self.working_tree().unknowns()
372
def commit(self, message, timestamp=None, timezone=None,
375
"""Commit working copy as a new revision.
377
The basic approach is to add all the file texts into the
378
store, then the inventory, then make a new revision pointing
379
to that inventory and store that.
381
This is not quite safe if the working copy changes during the
382
commit; for the moment that is simply not allowed. A better
383
approach is to make a temporary copy of the files before
384
computing their hashes, and then add those hashes in turn to
385
the inventory. This should mean at least that there are no
386
broken hash pointers. There is no way we can get a snapshot
387
of the whole directory at an instant. This would also have to
388
be robust against files disappearing, moving, etc. So the
389
whole thing is a bit hard.
391
:param timestamp: if not None, seconds-since-epoch for a
392
postdated/predated commit.
395
## TODO: Show branch names
397
# TODO: Don't commit if there are no changes, unless forced?
399
# First walk over the working inventory; and both update that
400
# and also build a new revision inventory. The revision
401
# inventory needs to hold the text-id, sha1 and size of the
402
# actual file versions committed in the revision. (These are
403
# not present in the working inventory.) We also need to
404
# detect missing/deleted files, and remove them from the
407
work_inv = self.read_working_inventory()
409
basis = self.basis_tree()
410
basis_inv = basis.inventory
412
for path, entry in work_inv.iter_entries():
413
## TODO: Cope with files that have gone missing.
415
## TODO: Check that the file kind has not changed from the previous
416
## revision of this file (if any).
420
p = self.abspath(path)
421
file_id = entry.file_id
422
mutter('commit prep file %s, id %r ' % (p, file_id))
424
if not os.path.exists(p):
425
mutter(" file is missing, removing from inventory")
427
show_status('D', entry.kind, quotefn(path))
428
missing_ids.append(file_id)
431
# TODO: Handle files that have been deleted
433
# TODO: Maybe a special case for empty files? Seems a
434
# waste to store them many times.
438
if basis_inv.has_id(file_id):
439
old_kind = basis_inv[file_id].kind
440
if old_kind != entry.kind:
441
bailout("entry %r changed kind from %r to %r"
442
% (file_id, old_kind, entry.kind))
444
if entry.kind == 'directory':
446
bailout("%s is entered as directory but not a directory" % quotefn(p))
447
elif entry.kind == 'file':
449
bailout("%s is entered as file but is not a file" % quotefn(p))
451
content = file(p, 'rb').read()
453
entry.text_sha1 = sha_string(content)
454
entry.text_size = len(content)
456
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
458
and (old_ie.text_size == entry.text_size)
459
and (old_ie.text_sha1 == entry.text_sha1)):
460
## assert content == basis.get_file(file_id).read()
461
entry.text_id = basis_inv[file_id].text_id
462
mutter(' unchanged from previous text_id {%s}' %
466
entry.text_id = _gen_file_id(entry.name)
467
self.text_store.add(content, entry.text_id)
468
mutter(' stored with text_id {%s}' % entry.text_id)
472
elif (old_ie.name == entry.name
473
and old_ie.parent_id == entry.parent_id):
478
show_status(state, entry.kind, quotefn(path))
480
for file_id in missing_ids:
481
# have to do this later so we don't mess up the iterator.
482
# since parents may be removed before their children we
485
# FIXME: There's probably a better way to do this; perhaps
486
# the workingtree should know how to filter itself.
487
if work_inv.has_id(file_id):
488
del work_inv[file_id]
491
inv_id = rev_id = _gen_revision_id(time.time())
493
inv_tmp = tempfile.TemporaryFile()
494
inv.write_xml(inv_tmp)
496
self.inventory_store.add(inv_tmp, inv_id)
497
mutter('new inventory_id is {%s}' % inv_id)
499
self._write_inventory(work_inv)
501
if timestamp == None:
502
timestamp = time.time()
504
if committer == None:
505
committer = username()
508
timezone = local_time_offset()
510
mutter("building commit log message")
511
rev = Revision(timestamp=timestamp,
514
precursor = self.last_patch(),
519
rev_tmp = tempfile.TemporaryFile()
520
rev.write_xml(rev_tmp)
522
self.revision_store.add(rev_tmp, rev_id)
523
mutter("new revision_id is {%s}" % rev_id)
525
## XXX: Everything up to here can simply be orphaned if we abort
526
## the commit; it will leave junk files behind but that doesn't
529
## TODO: Read back the just-generated changeset, and make sure it
530
## applies and recreates the right state.
532
## TODO: Also calculate and store the inventory SHA1
533
mutter("committing patch r%d" % (self.revno() + 1))
535
mutter("append to revision-history")
536
self.controlfile('revision-history', 'at').write(rev_id + '\n')
541
def get_revision(self, revision_id):
542
"""Return the Revision object for a named revision"""
543
r = Revision.read_xml(self.revision_store[revision_id])
544
assert r.revision_id == revision_id
548
def get_inventory(self, inventory_id):
549
"""Get Inventory object by hash.
551
:todo: Perhaps for this and similar methods, take a revision
552
parameter which can be either an integer revno or a
554
i = Inventory.read_xml(self.inventory_store[inventory_id])
558
def get_revision_inventory(self, revision_id):
559
"""Return inventory of a past revision."""
560
if revision_id == None:
563
return self.get_inventory(self.get_revision(revision_id).inventory_id)
566
def revision_history(self):
567
"""Return sequence of revision hashes on to this branch.
569
>>> ScratchBranch().revision_history()
572
return [chomp(l) for l in self.controlfile('revision-history').readlines()]
576
"""Return current revision number for this branch.
578
That is equivalent to the number of revisions committed to
581
>>> b = ScratchBranch()
584
>>> b.commit('no foo')
588
return len(self.revision_history())
591
def last_patch(self):
592
"""Return last patch hash, or None if no history.
594
>>> ScratchBranch().last_patch() == None
597
ph = self.revision_history()
602
def lookup_revision(self, revno):
603
"""Return revision hash for revision number."""
608
# list is 0-based; revisions are 1-based
609
return self.revision_history()[revno-1]
611
bailout("no such revision %s" % revno)
614
def revision_tree(self, revision_id):
615
"""Return Tree for a revision on this branch.
617
`revision_id` may be None for the null revision, in which case
618
an `EmptyTree` is returned."""
620
if revision_id == None:
623
inv = self.get_revision_inventory(revision_id)
624
return RevisionTree(self.text_store, inv)
627
def working_tree(self):
628
"""Return a `Tree` for the working copy."""
629
return WorkingTree(self.base, self.read_working_inventory())
632
def basis_tree(self):
633
"""Return `Tree` object for last revision.
635
If there are no revisions yet, return an `EmptyTree`.
637
>>> b = ScratchBranch(files=['foo'])
638
>>> b.basis_tree().has_filename('foo')
640
>>> b.working_tree().has_filename('foo')
643
>>> b.commit('add foo')
644
>>> b.basis_tree().has_filename('foo')
647
r = self.last_patch()
651
return RevisionTree(self.text_store, self.get_revision_inventory(r))
655
def write_log(self, show_timezone='original'):
656
"""Write out human-readable log of commits to this branch
658
:param utc: If true, show dates in universal time, not local time."""
659
## TODO: Option to choose either original, utc or local timezone
662
for p in self.revision_history():
664
print 'revno:', revno
665
## TODO: Show hash if --id is given.
666
##print 'revision-hash:', p
667
rev = self.get_revision(p)
668
print 'committer:', rev.committer
669
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
672
## opportunistic consistency check, same as check_patch_chaining
673
if rev.precursor != precursor:
674
bailout("mismatched precursor!")
678
print ' (no message)'
680
for l in rev.message.split('\n'):
688
def show_status(branch, show_all=False):
689
"""Display single-line status for non-ignored working files.
691
The list is show sorted in order by file name.
693
>>> b = ScratchBranch(files=['foo', 'foo~'])
699
>>> b.commit("add foo")
701
>>> os.unlink(b.abspath('foo'))
706
:todo: Get state for single files.
708
:todo: Perhaps show a slash at the end of directory names.
712
# We have to build everything into a list first so that it can
713
# sorted by name, incorporating all the different sources.
715
# FIXME: Rather than getting things in random order and then sorting,
716
# just step through in order.
718
# Interesting case: the old ID for a file has been removed,
719
# but a new file has been created under that name.
721
old = branch.basis_tree()
722
old_inv = old.inventory
723
new = branch.working_tree()
724
new_inv = new.inventory
726
for fs, fid, oldname, newname, kind in diff_trees(old, new):
728
show_status(fs, kind,
729
oldname + ' => ' + newname)
730
elif fs == 'A' or fs == 'M':
731
show_status(fs, kind, newname)
733
show_status(fs, kind, oldname)
736
show_status(fs, kind, newname)
739
show_status(fs, kind, newname)
741
show_status(fs, kind, newname)
743
bailout("wierd file state %r" % ((fs, fid),))
747
class ScratchBranch(Branch):
748
"""Special test class: a branch that cleans up after itself.
750
>>> b = ScratchBranch()
758
def __init__(self, files = []):
759
"""Make a test branch.
761
This creates a temporary directory and runs init-tree in it.
763
If any files are listed, they are created in the working copy.
765
Branch.__init__(self, tempfile.mkdtemp(), init=True)
767
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
771
"""Destroy the test branch, removing the scratch directory."""
772
shutil.rmtree(self.base)
776
######################################################################
780
def is_control_file(filename):
781
## FIXME: better check
782
filename = os.path.normpath(filename)
783
while filename != '':
784
head, tail = os.path.split(filename)
785
## mutter('check %r for control file' % ((head, tail), ))
786
if tail == bzrlib.BZRDIR:
793
def _gen_revision_id(when):
794
"""Return new revision-id."""
795
s = '%s-%s-' % (user_email(), compact_date(when))
796
s += hexlify(rand_bytes(8))
800
def _gen_file_id(name):
801
"""Return new file id.
803
This should probably generate proper UUIDs, but for the moment we
804
cope with just randomness because running uuidgen every time is
806
assert '/' not in name
807
while name[0] == '.':
809
s = hexlify(rand_bytes(8))
810
return '-'.join((name, compact_date(time.time()), s))