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.
106
self.base = find_branch_root(base)
108
self.base = os.path.realpath(base)
109
if not isdir(self.controlfilename('.')):
110
bailout("not a bzr branch: %s" % quotefn(base),
111
['use "bzr init" to initialize a new working tree',
112
'current bzr can only operate from top-of-tree'])
115
self.text_store = ImmutableStore(self.controlfilename('text-store'))
116
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
117
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
121
return '%s(%r)' % (self.__class__.__name__, self.base)
127
def _rel(self, name):
128
"""Return filename relative to branch top"""
129
return os.path.join(self.base, name)
132
def controlfilename(self, file_or_path):
133
"""Return location relative to branch."""
134
if isinstance(file_or_path, types.StringTypes):
135
file_or_path = [file_or_path]
136
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
139
def controlfile(self, file_or_path, mode='r'):
140
"""Open a control file for this branch"""
141
return file(self.controlfilename(file_or_path), mode)
144
def _make_control(self):
145
os.mkdir(self.controlfilename([]))
146
self.controlfile('README', 'w').write(
147
"This is a Bazaar-NG control directory.\n"
148
"Do not change any files in this directory.")
149
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
150
for d in ('text-store', 'inventory-store', 'revision-store'):
151
os.mkdir(self.controlfilename(d))
152
for f in ('revision-history', 'merged-patches',
153
'pending-merged-patches', 'branch-name'):
154
self.controlfile(f, 'w').write('')
155
mutter('created control directory in ' + self.base)
156
Inventory().write_xml(self.controlfile('inventory','w'))
159
def _check_format(self):
160
"""Check this branch format is supported.
162
The current tool only supports the current unstable format.
164
In the future, we might need different in-memory Branch
165
classes to support downlevel branches. But not yet.
167
# read in binary mode to detect newline wierdness.
168
fmt = self.controlfile('branch-format', 'rb').read()
169
if fmt != BZR_BRANCH_FORMAT:
170
bailout('sorry, branch format %r not supported' % fmt,
171
['use a different bzr version',
172
'or remove the .bzr directory and "bzr init" again'])
175
def read_working_inventory(self):
176
"""Read the working inventory."""
178
inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
179
mutter("loaded inventory of %d items in %f"
180
% (len(inv), time.time() - before))
184
def _write_inventory(self, inv):
185
"""Update the working inventory.
187
That is to say, the inventory describing changes underway, that
188
will be committed to the next revision.
190
## TODO: factor out to atomicfile? is rename safe on windows?
191
tmpfname = self.controlfilename('inventory.tmp')
192
tmpf = file(tmpfname, 'w')
195
os.rename(tmpfname, self.controlfilename('inventory'))
196
mutter('wrote working inventory')
199
inventory = property(read_working_inventory, _write_inventory, None,
200
"""Inventory for the working copy.""")
203
def add(self, files, verbose=False):
204
"""Make files versioned.
206
This puts the files in the Added state, so that they will be
207
recorded by the next commit.
209
:todo: Perhaps have an option to add the ids even if the files do
212
:todo: Perhaps return the ids of the files? But then again it
213
is easy to retrieve them if they're needed.
215
:todo: Option to specify file id.
217
:todo: Adding a directory should optionally recurse down and
218
add all non-ignored children. Perhaps do that in a
221
>>> b = ScratchBranch(files=['foo'])
222
>>> 'foo' in b.unknowns()
227
>>> 'foo' in b.unknowns()
229
>>> bool(b.inventory.path2id('foo'))
235
Traceback (most recent call last):
237
BzrError: ('foo is already versioned', [])
239
>>> b.add(['nothere'])
240
Traceback (most recent call last):
241
BzrError: ('cannot add: not a regular file or directory: nothere', [])
244
# TODO: Re-adding a file that is removed in the working copy
245
# should probably put it back with the previous ID.
246
if isinstance(files, types.StringTypes):
249
inv = self.read_working_inventory()
251
if is_control_file(f):
252
bailout("cannot add control file %s" % quotefn(f))
257
bailout("cannot add top-level %r" % f)
259
fullpath = os.path.normpath(self._rel(f))
263
elif isdir(fullpath):
266
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
269
parent_name = joinpath(fp[:-1])
270
mutter("lookup parent %r" % parent_name)
271
parent_id = inv.path2id(parent_name)
272
if parent_id == None:
273
bailout("cannot add: parent %r is not versioned"
278
file_id = _gen_file_id(fp[-1])
279
inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
281
show_status('A', kind, quotefn(f))
283
mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
284
% (f, file_id, kind, parent_id))
285
self._write_inventory(inv)
289
def remove(self, files, verbose=False):
290
"""Mark nominated files for removal from the inventory.
292
This does not remove their text. This does not run on
294
:todo: Refuse to remove modified files unless --force is given?
296
>>> b = ScratchBranch(files=['foo'])
298
>>> b.inventory.has_filename('foo')
301
>>> b.working_tree().has_filename('foo')
303
>>> b.inventory.has_filename('foo')
306
>>> b = ScratchBranch(files=['foo'])
311
>>> b.inventory.has_filename('foo')
313
>>> b.basis_tree().has_filename('foo')
315
>>> b.working_tree().has_filename('foo')
318
:todo: Do something useful with directories.
320
:todo: Should this remove the text or not? Tough call; not
321
removing may be useful and the user can just use use rm, and
322
is the opposite of add. Removing it is consistent with most
323
other tools. Maybe an option.
325
## TODO: Normalize names
326
## TODO: Remove nested loops; better scalability
328
if isinstance(files, types.StringTypes):
331
tree = self.working_tree()
334
# do this before any modifications
338
bailout("cannot remove unversioned file %s" % quotefn(f))
339
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
341
# having remove it, it must be either ignored or unknown
342
if tree.is_ignored(f):
346
show_status(new_status, inv[fid].kind, quotefn(f))
349
self._write_inventory(inv)
353
"""Return all unknown files.
355
These are files in the working directory that are not versioned or
356
control files or ignored.
358
>>> b = ScratchBranch(files=['foo', 'foo~'])
359
>>> list(b.unknowns())
362
>>> list(b.unknowns())
365
>>> list(b.unknowns())
368
return self.working_tree().unknowns()
371
def commit(self, message, timestamp=None, timezone=None,
374
"""Commit working copy as a new revision.
376
The basic approach is to add all the file texts into the
377
store, then the inventory, then make a new revision pointing
378
to that inventory and store that.
380
This is not quite safe if the working copy changes during the
381
commit; for the moment that is simply not allowed. A better
382
approach is to make a temporary copy of the files before
383
computing their hashes, and then add those hashes in turn to
384
the inventory. This should mean at least that there are no
385
broken hash pointers. There is no way we can get a snapshot
386
of the whole directory at an instant. This would also have to
387
be robust against files disappearing, moving, etc. So the
388
whole thing is a bit hard.
390
:param timestamp: if not None, seconds-since-epoch for a
391
postdated/predated commit.
394
## TODO: Show branch names
396
# TODO: Don't commit if there are no changes, unless forced?
398
# First walk over the working inventory; and both update that
399
# and also build a new revision inventory. The revision
400
# inventory needs to hold the text-id, sha1 and size of the
401
# actual file versions committed in the revision. (These are
402
# not present in the working inventory.) We also need to
403
# detect missing/deleted files, and remove them from the
406
work_inv = self.read_working_inventory()
408
basis = self.basis_tree()
409
basis_inv = basis.inventory
411
for path, entry in work_inv.iter_entries():
412
## TODO: Cope with files that have gone missing.
414
## TODO: Check that the file kind has not changed from the previous
415
## revision of this file (if any).
420
file_id = entry.file_id
421
mutter('commit prep file %s, id %r ' % (p, file_id))
423
if not os.path.exists(p):
424
mutter(" file is missing, removing from inventory")
426
show_status('D', entry.kind, quotefn(path))
427
missing_ids.append(file_id)
430
# TODO: Handle files that have been deleted
432
# TODO: Maybe a special case for empty files? Seems a
433
# waste to store them many times.
437
if basis_inv.has_id(file_id):
438
old_kind = basis_inv[file_id].kind
439
if old_kind != entry.kind:
440
bailout("entry %r changed kind from %r to %r"
441
% (file_id, old_kind, entry.kind))
443
if entry.kind == 'directory':
445
bailout("%s is entered as directory but not a directory" % quotefn(p))
446
elif entry.kind == 'file':
448
bailout("%s is entered as file but is not a file" % quotefn(p))
450
content = file(p, 'rb').read()
452
entry.text_sha1 = sha_string(content)
453
entry.text_size = len(content)
455
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
457
and (old_ie.text_size == entry.text_size)
458
and (old_ie.text_sha1 == entry.text_sha1)):
459
## assert content == basis.get_file(file_id).read()
460
entry.text_id = basis_inv[file_id].text_id
461
mutter(' unchanged from previous text_id {%s}' %
465
entry.text_id = _gen_file_id(entry.name)
466
self.text_store.add(content, entry.text_id)
467
mutter(' stored with text_id {%s}' % entry.text_id)
471
elif (old_ie.name == entry.name
472
and old_ie.parent_id == entry.parent_id):
477
show_status(state, entry.kind, quotefn(path))
479
for file_id in missing_ids:
480
# have to do this later so we don't mess up the iterator.
481
# since parents may be removed before their children we
484
# FIXME: There's probably a better way to do this; perhaps
485
# the workingtree should know how to filter itself.
486
if work_inv.has_id(file_id):
487
del work_inv[file_id]
490
inv_id = rev_id = _gen_revision_id(time.time())
492
inv_tmp = tempfile.TemporaryFile()
493
inv.write_xml(inv_tmp)
495
self.inventory_store.add(inv_tmp, inv_id)
496
mutter('new inventory_id is {%s}' % inv_id)
498
self._write_inventory(work_inv)
500
if timestamp == None:
501
timestamp = time.time()
503
if committer == None:
504
committer = username()
507
timezone = local_time_offset()
509
mutter("building commit log message")
510
rev = Revision(timestamp=timestamp,
513
precursor = self.last_patch(),
518
rev_tmp = tempfile.TemporaryFile()
519
rev.write_xml(rev_tmp)
521
self.revision_store.add(rev_tmp, rev_id)
522
mutter("new revision_id is {%s}" % rev_id)
524
## XXX: Everything up to here can simply be orphaned if we abort
525
## the commit; it will leave junk files behind but that doesn't
528
## TODO: Read back the just-generated changeset, and make sure it
529
## applies and recreates the right state.
531
## TODO: Also calculate and store the inventory SHA1
532
mutter("committing patch r%d" % (self.revno() + 1))
534
mutter("append to revision-history")
535
self.controlfile('revision-history', 'at').write(rev_id + '\n')
540
def get_revision(self, revision_id):
541
"""Return the Revision object for a named revision"""
542
r = Revision.read_xml(self.revision_store[revision_id])
543
assert r.revision_id == revision_id
547
def get_inventory(self, inventory_id):
548
"""Get Inventory object by hash.
550
:todo: Perhaps for this and similar methods, take a revision
551
parameter which can be either an integer revno or a
553
i = Inventory.read_xml(self.inventory_store[inventory_id])
557
def get_revision_inventory(self, revision_id):
558
"""Return inventory of a past revision."""
559
if revision_id == None:
562
return self.get_inventory(self.get_revision(revision_id).inventory_id)
565
def revision_history(self):
566
"""Return sequence of revision hashes on to this branch.
568
>>> ScratchBranch().revision_history()
571
return [chomp(l) for l in self.controlfile('revision-history').readlines()]
575
"""Return current revision number for this branch.
577
That is equivalent to the number of revisions committed to
580
>>> b = ScratchBranch()
583
>>> b.commit('no foo')
587
return len(self.revision_history())
590
def last_patch(self):
591
"""Return last patch hash, or None if no history.
593
>>> ScratchBranch().last_patch() == None
596
ph = self.revision_history()
601
def lookup_revision(self, revno):
602
"""Return revision hash for revision number."""
607
# list is 0-based; revisions are 1-based
608
return self.revision_history()[revno-1]
610
bailout("no such revision %s" % revno)
613
def revision_tree(self, revision_id):
614
"""Return Tree for a revision on this branch.
616
`revision_id` may be None for the null revision, in which case
617
an `EmptyTree` is returned."""
619
if revision_id == None:
622
inv = self.get_revision_inventory(revision_id)
623
return RevisionTree(self.text_store, inv)
626
def working_tree(self):
627
"""Return a `Tree` for the working copy."""
628
return WorkingTree(self.base, self.read_working_inventory())
631
def basis_tree(self):
632
"""Return `Tree` object for last revision.
634
If there are no revisions yet, return an `EmptyTree`.
636
>>> b = ScratchBranch(files=['foo'])
637
>>> b.basis_tree().has_filename('foo')
639
>>> b.working_tree().has_filename('foo')
642
>>> b.commit('add foo')
643
>>> b.basis_tree().has_filename('foo')
646
r = self.last_patch()
650
return RevisionTree(self.text_store, self.get_revision_inventory(r))
654
def write_log(self, show_timezone='original'):
655
"""Write out human-readable log of commits to this branch
657
:param utc: If true, show dates in universal time, not local time."""
658
## TODO: Option to choose either original, utc or local timezone
661
for p in self.revision_history():
663
print 'revno:', revno
664
## TODO: Show hash if --id is given.
665
##print 'revision-hash:', p
666
rev = self.get_revision(p)
667
print 'committer:', rev.committer
668
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
671
## opportunistic consistency check, same as check_patch_chaining
672
if rev.precursor != precursor:
673
bailout("mismatched precursor!")
677
print ' (no message)'
679
for l in rev.message.split('\n'):
687
def show_status(branch, show_all=False):
688
"""Display single-line status for non-ignored working files.
690
The list is show sorted in order by file name.
692
>>> b = ScratchBranch(files=['foo', 'foo~'])
698
>>> b.commit("add foo")
700
>>> os.unlink(b._rel('foo'))
705
:todo: Get state for single files.
707
:todo: Perhaps show a slash at the end of directory names.
711
# We have to build everything into a list first so that it can
712
# sorted by name, incorporating all the different sources.
714
# FIXME: Rather than getting things in random order and then sorting,
715
# just step through in order.
717
# Interesting case: the old ID for a file has been removed,
718
# but a new file has been created under that name.
720
old = branch.basis_tree()
721
old_inv = old.inventory
722
new = branch.working_tree()
723
new_inv = new.inventory
725
for fs, fid, oldname, newname, kind in diff_trees(old, new):
727
show_status(fs, kind,
728
oldname + ' => ' + newname)
729
elif fs == 'A' or fs == 'M':
730
show_status(fs, kind, newname)
732
show_status(fs, kind, oldname)
735
show_status(fs, kind, newname)
738
show_status(fs, kind, newname)
740
show_status(fs, kind, newname)
742
bailout("wierd file state %r" % ((fs, fid),))
746
class ScratchBranch(Branch):
747
"""Special test class: a branch that cleans up after itself.
749
>>> b = ScratchBranch()
757
def __init__(self, files = []):
758
"""Make a test branch.
760
This creates a temporary directory and runs init-tree in it.
762
If any files are listed, they are created in the working copy.
764
Branch.__init__(self, tempfile.mkdtemp(), init=True)
766
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
770
"""Destroy the test branch, removing the scratch directory."""
771
shutil.rmtree(self.base)
775
######################################################################
779
def is_control_file(filename):
780
## FIXME: better check
781
filename = os.path.normpath(filename)
782
while filename != '':
783
head, tail = os.path.split(filename)
784
## mutter('check %r for control file' % ((head, tail), ))
785
if tail == bzrlib.BZRDIR:
792
def _gen_revision_id(when):
793
"""Return new revision-id."""
794
s = '%s-%s-' % (user_email(), compact_date(when))
795
s += hexlify(rand_bytes(8))
799
def _gen_file_id(name):
800
"""Return new file id.
802
This should probably generate proper UUIDs, but for the moment we
803
cope with just randomness because running uuidgen every time is
805
assert '/' not in name
806
while name[0] == '.':
808
s = hexlify(rand_bytes(8))
809
return '-'.join((name, compact_date(time.time()), s))