1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# 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
26
from inventory import InventoryEntry, Inventory
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
joinpath, sha_string, file_kind, local_time_offset, appendpath
30
from store import ImmutableStore
31
from revision import Revision
32
from errors import BzrError
33
from textui import show_status
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
## 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 _relpath(base, path):
50
"""Return path relative to base, or raise exception.
52
The path may be either an absolute path or a path relative to the
53
current working directory.
55
Lifted out of Branch.relpath for ease of testing.
57
os.path.commonprefix (python2.4) has a bad bug that it works just
58
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
59
avoids that problem."""
60
rp = os.path.abspath(path)
64
while len(head) >= len(base):
67
head, tail = os.path.split(head)
71
from errors import NotBranchError
72
raise NotBranchError("path %r is not within branch %r" % (rp, base))
77
def find_branch_root(f=None):
78
"""Find the branch root enclosing f, or pwd.
80
f may be a filename or a URL.
82
It is not necessary that f exists.
84
Basically we keep looking up until we find the control directory or
88
elif hasattr(os.path, 'realpath'):
89
f = os.path.realpath(f)
91
f = os.path.abspath(f)
92
if not os.path.exists(f):
93
raise BzrError('%r does not exist' % f)
99
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
101
head, tail = os.path.split(f)
103
# reached the root, whatever that may be
104
raise BzrError('%r is not in a branch' % orig_f)
109
######################################################################
112
class Branch(object):
113
"""Branch holding a history of revisions.
116
Base directory of the branch.
122
If _lock_mode is true, a positive count of the number of times the
126
Lock object from bzrlib.lock.
133
def __init__(self, base, init=False, find_root=True):
134
"""Create new branch object at a particular location.
136
base -- Base directory for the branch.
138
init -- If True, create new control files in a previously
139
unversioned directory. If False, the branch must already
142
find_root -- If true and init is false, find the root of the
143
existing branch containing base.
145
In the test suite, creation of new trees is tested using the
146
`ScratchBranch` class.
149
self.base = os.path.realpath(base)
152
self.base = find_branch_root(base)
154
self.base = os.path.realpath(base)
155
if not isdir(self.controlfilename('.')):
156
from errors import NotBranchError
157
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
158
['use "bzr init" to initialize a new working tree',
159
'current bzr can only operate from top-of-tree'])
162
self.text_store = ImmutableStore(self.controlfilename('text-store'))
163
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
164
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
168
return '%s(%r)' % (self.__class__.__name__, self.base)
175
if self._lock_mode or self._lock:
176
from warnings import warn
177
warn("branch %r was not explicitly unlocked" % self)
182
def lock_write(self):
184
if self._lock_mode != 'w':
185
from errors import LockError
186
raise LockError("can't upgrade to a write lock from %r" %
188
self._lock_count += 1
190
from bzrlib.lock import WriteLock
192
self._lock = WriteLock(self.controlfilename('branch-lock'))
193
self._lock_mode = 'w'
200
assert self._lock_mode in ('r', 'w'), \
201
"invalid lock mode %r" % self._lock_mode
202
self._lock_count += 1
204
from bzrlib.lock import ReadLock
206
self._lock = ReadLock(self.controlfilename('branch-lock'))
207
self._lock_mode = 'r'
213
if not self._lock_mode:
214
from errors import LockError
215
raise LockError('branch %r is not locked' % (self))
217
if self._lock_count > 1:
218
self._lock_count -= 1
222
self._lock_mode = self._lock_count = None
225
def abspath(self, name):
226
"""Return absolute filename for something in the branch"""
227
return os.path.join(self.base, name)
230
def relpath(self, path):
231
"""Return path relative to this branch of something inside it.
233
Raises an error if path is not in this branch."""
234
return _relpath(self.base, path)
237
def controlfilename(self, file_or_path):
238
"""Return location relative to branch."""
239
if isinstance(file_or_path, types.StringTypes):
240
file_or_path = [file_or_path]
241
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
244
def controlfile(self, file_or_path, mode='r'):
245
"""Open a control file for this branch.
247
There are two classes of file in the control directory: text
248
and binary. binary files are untranslated byte streams. Text
249
control files are stored with Unix newlines and in UTF-8, even
250
if the platform or locale defaults are different.
252
Controlfiles should almost never be opened in write mode but
253
rather should be atomically copied and replaced using atomicfile.
256
fn = self.controlfilename(file_or_path)
258
if mode == 'rb' or mode == 'wb':
259
return file(fn, mode)
260
elif mode == 'r' or mode == 'w':
261
# open in binary mode anyhow so there's no newline translation;
262
# codecs uses line buffering by default; don't want that.
264
return codecs.open(fn, mode + 'b', 'utf-8',
267
raise BzrError("invalid controlfile mode %r" % mode)
271
def _make_control(self):
272
os.mkdir(self.controlfilename([]))
273
self.controlfile('README', 'w').write(
274
"This is a Bazaar-NG control directory.\n"
275
"Do not change any files in this directory.")
276
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
277
for d in ('text-store', 'inventory-store', 'revision-store'):
278
os.mkdir(self.controlfilename(d))
279
for f in ('revision-history', 'merged-patches',
280
'pending-merged-patches', 'branch-name',
282
self.controlfile(f, 'w').write('')
283
mutter('created control directory in ' + self.base)
284
Inventory().write_xml(self.controlfile('inventory','w'))
287
def _check_format(self):
288
"""Check this branch format is supported.
290
The current tool only supports the current unstable format.
292
In the future, we might need different in-memory Branch
293
classes to support downlevel branches. But not yet.
295
# This ignores newlines so that we can open branches created
296
# on Windows from Linux and so on. I think it might be better
297
# to always make all internal files in unix format.
298
fmt = self.controlfile('branch-format', 'r').read()
299
fmt.replace('\r\n', '')
300
if fmt != BZR_BRANCH_FORMAT:
301
raise BzrError('sorry, branch format %r not supported' % fmt,
302
['use a different bzr version',
303
'or remove the .bzr directory and "bzr init" again'])
307
def read_working_inventory(self):
308
"""Read the working inventory."""
310
# ElementTree does its own conversion from UTF-8, so open in
314
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
315
mutter("loaded inventory of %d items in %f"
316
% (len(inv), time.time() - before))
322
def _write_inventory(self, inv):
323
"""Update the working inventory.
325
That is to say, the inventory describing changes underway, that
326
will be committed to the next revision.
328
## TODO: factor out to atomicfile? is rename safe on windows?
329
## TODO: Maybe some kind of clean/dirty marker on inventory?
330
tmpfname = self.controlfilename('inventory.tmp')
331
tmpf = file(tmpfname, 'wb')
334
inv_fname = self.controlfilename('inventory')
335
if sys.platform == 'win32':
337
os.rename(tmpfname, inv_fname)
338
mutter('wrote working inventory')
341
inventory = property(read_working_inventory, _write_inventory, None,
342
"""Inventory for the working copy.""")
345
def add(self, files, verbose=False, ids=None):
346
"""Make files versioned.
348
Note that the command line normally calls smart_add instead.
350
This puts the files in the Added state, so that they will be
351
recorded by the next commit.
354
List of paths to add, relative to the base of the tree.
357
If set, use these instead of automatically generated ids.
358
Must be the same length as the list of files, but may
359
contain None for ids that are to be autogenerated.
361
TODO: Perhaps have an option to add the ids even if the files do
364
TODO: Perhaps return the ids of the files? But then again it
365
is easy to retrieve them if they're needed.
367
TODO: Adding a directory should optionally recurse down and
368
add all non-ignored children. Perhaps do that in a
371
# TODO: Re-adding a file that is removed in the working copy
372
# should probably put it back with the previous ID.
373
if isinstance(files, types.StringTypes):
374
assert(ids is None or isinstance(ids, types.StringTypes))
380
ids = [None] * len(files)
382
assert(len(ids) == len(files))
386
inv = self.read_working_inventory()
387
for f,file_id in zip(files, ids):
388
if is_control_file(f):
389
raise BzrError("cannot add control file %s" % quotefn(f))
394
raise BzrError("cannot add top-level %r" % f)
396
fullpath = os.path.normpath(self.abspath(f))
399
kind = file_kind(fullpath)
401
# maybe something better?
402
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
404
if kind != 'file' and kind != 'directory':
405
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
408
file_id = gen_file_id(f)
409
inv.add_path(f, kind=kind, file_id=file_id)
412
show_status('A', kind, quotefn(f))
414
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
416
self._write_inventory(inv)
421
def print_file(self, file, revno):
422
"""Print `file` to stdout."""
425
tree = self.revision_tree(self.lookup_revision(revno))
426
# use inventory as it was in that revision
427
file_id = tree.inventory.path2id(file)
429
raise BzrError("%r is not present in revision %d" % (file, revno))
430
tree.print_file(file_id)
435
def remove(self, files, verbose=False):
436
"""Mark nominated files for removal from the inventory.
438
This does not remove their text. This does not run on
440
TODO: Refuse to remove modified files unless --force is given?
442
TODO: Do something useful with directories.
444
TODO: Should this remove the text or not? Tough call; not
445
removing may be useful and the user can just use use rm, and
446
is the opposite of add. Removing it is consistent with most
447
other tools. Maybe an option.
449
## TODO: Normalize names
450
## TODO: Remove nested loops; better scalability
451
if isinstance(files, types.StringTypes):
457
tree = self.working_tree()
460
# do this before any modifications
464
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
465
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
467
# having remove it, it must be either ignored or unknown
468
if tree.is_ignored(f):
472
show_status(new_status, inv[fid].kind, quotefn(f))
475
self._write_inventory(inv)
480
# FIXME: this doesn't need to be a branch method
481
def set_inventory(self, new_inventory_list):
483
for path, file_id, parent, kind in new_inventory_list:
484
name = os.path.basename(path)
487
inv.add(InventoryEntry(file_id, name, kind, parent))
488
self._write_inventory(inv)
492
"""Return all unknown files.
494
These are files in the working directory that are not versioned or
495
control files or ignored.
497
>>> b = ScratchBranch(files=['foo', 'foo~'])
498
>>> list(b.unknowns())
501
>>> list(b.unknowns())
504
>>> list(b.unknowns())
507
return self.working_tree().unknowns()
510
def append_revision(self, revision_id):
511
mutter("add {%s} to revision-history" % revision_id)
512
rev_history = self.revision_history()
514
tmprhname = self.controlfilename('revision-history.tmp')
515
rhname = self.controlfilename('revision-history')
517
f = file(tmprhname, 'wt')
518
rev_history.append(revision_id)
519
f.write('\n'.join(rev_history))
523
if sys.platform == 'win32':
525
os.rename(tmprhname, rhname)
529
def get_revision(self, revision_id):
530
"""Return the Revision object for a named revision"""
531
r = Revision.read_xml(self.revision_store[revision_id])
532
assert r.revision_id == revision_id
536
def get_inventory(self, inventory_id):
537
"""Get Inventory object by hash.
539
TODO: Perhaps for this and similar methods, take a revision
540
parameter which can be either an integer revno or a
542
i = Inventory.read_xml(self.inventory_store[inventory_id])
546
def get_revision_inventory(self, revision_id):
547
"""Return inventory of a past revision."""
548
if revision_id == None:
551
return self.get_inventory(self.get_revision(revision_id).inventory_id)
554
def revision_history(self):
555
"""Return sequence of revision hashes on to this branch.
557
>>> ScratchBranch().revision_history()
562
return [l.rstrip('\r\n') for l in
563
self.controlfile('revision-history', 'r').readlines()]
568
def common_ancestor(self, other, self_revno=None, other_revno=None):
571
>>> sb = ScratchBranch(files=['foo', 'foo~'])
572
>>> sb.common_ancestor(sb) == (None, None)
574
>>> commit.commit(sb, "Committing first revision", verbose=False)
575
>>> sb.common_ancestor(sb)[0]
577
>>> clone = sb.clone()
578
>>> commit.commit(sb, "Committing second revision", verbose=False)
579
>>> sb.common_ancestor(sb)[0]
581
>>> sb.common_ancestor(clone)[0]
583
>>> commit.commit(clone, "Committing divergent second revision",
585
>>> sb.common_ancestor(clone)[0]
587
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
589
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
591
>>> clone2 = sb.clone()
592
>>> sb.common_ancestor(clone2)[0]
594
>>> sb.common_ancestor(clone2, self_revno=1)[0]
596
>>> sb.common_ancestor(clone2, other_revno=1)[0]
599
my_history = self.revision_history()
600
other_history = other.revision_history()
601
if self_revno is None:
602
self_revno = len(my_history)
603
if other_revno is None:
604
other_revno = len(other_history)
605
indices = range(min((self_revno, other_revno)))
608
if my_history[r] == other_history[r]:
609
return r+1, my_history[r]
612
def enum_history(self, direction):
613
"""Return (revno, revision_id) for history of branch.
616
'forward' is from earliest to latest
617
'reverse' is from latest to earliest
619
rh = self.revision_history()
620
if direction == 'forward':
625
elif direction == 'reverse':
631
raise ValueError('invalid history direction', direction)
635
"""Return current revision number for this branch.
637
That is equivalent to the number of revisions committed to
640
return len(self.revision_history())
643
def last_patch(self):
644
"""Return last patch hash, or None if no history.
646
ph = self.revision_history()
653
def commit(self, *args, **kw):
655
from bzrlib.commit import commit
656
commit(self, *args, **kw)
659
def lookup_revision(self, revno):
660
"""Return revision hash for revision number."""
665
# list is 0-based; revisions are 1-based
666
return self.revision_history()[revno-1]
668
raise BzrError("no such revision %s" % revno)
671
def revision_tree(self, revision_id):
672
"""Return Tree for a revision on this branch.
674
`revision_id` may be None for the null revision, in which case
675
an `EmptyTree` is returned."""
676
# TODO: refactor this to use an existing revision object
677
# so we don't need to read it in twice.
678
if revision_id == None:
681
inv = self.get_revision_inventory(revision_id)
682
return RevisionTree(self.text_store, inv)
685
def working_tree(self):
686
"""Return a `Tree` for the working copy."""
687
from workingtree import WorkingTree
688
return WorkingTree(self.base, self.read_working_inventory())
691
def basis_tree(self):
692
"""Return `Tree` object for last revision.
694
If there are no revisions yet, return an `EmptyTree`.
696
r = self.last_patch()
700
return RevisionTree(self.text_store, self.get_revision_inventory(r))
704
def rename_one(self, from_rel, to_rel):
707
This can change the directory or the filename or both.
711
tree = self.working_tree()
713
if not tree.has_filename(from_rel):
714
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
715
if tree.has_filename(to_rel):
716
raise BzrError("can't rename: new working file %r already exists" % to_rel)
718
file_id = inv.path2id(from_rel)
720
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
722
if inv.path2id(to_rel):
723
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
725
to_dir, to_tail = os.path.split(to_rel)
726
to_dir_id = inv.path2id(to_dir)
727
if to_dir_id == None and to_dir != '':
728
raise BzrError("can't determine destination directory id for %r" % to_dir)
730
mutter("rename_one:")
731
mutter(" file_id {%s}" % file_id)
732
mutter(" from_rel %r" % from_rel)
733
mutter(" to_rel %r" % to_rel)
734
mutter(" to_dir %r" % to_dir)
735
mutter(" to_dir_id {%s}" % to_dir_id)
737
inv.rename(file_id, to_dir_id, to_tail)
739
print "%s => %s" % (from_rel, to_rel)
741
from_abs = self.abspath(from_rel)
742
to_abs = self.abspath(to_rel)
744
os.rename(from_abs, to_abs)
746
raise BzrError("failed to rename %r to %r: %s"
747
% (from_abs, to_abs, e[1]),
748
["rename rolled back"])
750
self._write_inventory(inv)
755
def move(self, from_paths, to_name):
758
to_name must exist as a versioned directory.
760
If to_name exists and is a directory, the files are moved into
761
it, keeping their old names. If it is a directory,
763
Note that to_name is only the last component of the new name;
764
this doesn't change the directory.
768
## TODO: Option to move IDs only
769
assert not isinstance(from_paths, basestring)
770
tree = self.working_tree()
772
to_abs = self.abspath(to_name)
773
if not isdir(to_abs):
774
raise BzrError("destination %r is not a directory" % to_abs)
775
if not tree.has_filename(to_name):
776
raise BzrError("destination %r not in working directory" % to_abs)
777
to_dir_id = inv.path2id(to_name)
778
if to_dir_id == None and to_name != '':
779
raise BzrError("destination %r is not a versioned directory" % to_name)
780
to_dir_ie = inv[to_dir_id]
781
if to_dir_ie.kind not in ('directory', 'root_directory'):
782
raise BzrError("destination %r is not a directory" % to_abs)
784
to_idpath = inv.get_idpath(to_dir_id)
787
if not tree.has_filename(f):
788
raise BzrError("%r does not exist in working tree" % f)
789
f_id = inv.path2id(f)
791
raise BzrError("%r is not versioned" % f)
792
name_tail = splitpath(f)[-1]
793
dest_path = appendpath(to_name, name_tail)
794
if tree.has_filename(dest_path):
795
raise BzrError("destination %r already exists" % dest_path)
796
if f_id in to_idpath:
797
raise BzrError("can't move %r to a subdirectory of itself" % f)
799
# OK, so there's a race here, it's possible that someone will
800
# create a file in this interval and then the rename might be
801
# left half-done. But we should have caught most problems.
804
name_tail = splitpath(f)[-1]
805
dest_path = appendpath(to_name, name_tail)
806
print "%s => %s" % (f, dest_path)
807
inv.rename(inv.path2id(f), to_dir_id, name_tail)
809
os.rename(self.abspath(f), self.abspath(dest_path))
811
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
812
["rename rolled back"])
814
self._write_inventory(inv)
820
class ScratchBranch(Branch):
821
"""Special test class: a branch that cleans up after itself.
823
>>> b = ScratchBranch()
831
def __init__(self, files=[], dirs=[], base=None):
832
"""Make a test branch.
834
This creates a temporary directory and runs init-tree in it.
836
If any files are listed, they are created in the working copy.
840
base = tempfile.mkdtemp()
842
Branch.__init__(self, base, init=init)
844
os.mkdir(self.abspath(d))
847
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
852
>>> orig = ScratchBranch(files=["file1", "file2"])
853
>>> clone = orig.clone()
854
>>> os.path.samefile(orig.base, clone.base)
856
>>> os.path.isfile(os.path.join(clone.base, "file1"))
859
base = tempfile.mkdtemp()
861
shutil.copytree(self.base, base, symlinks=True)
862
return ScratchBranch(base=base)
868
"""Destroy the test branch, removing the scratch directory."""
871
mutter("delete ScratchBranch %s" % self.base)
872
shutil.rmtree(self.base)
874
# Work around for shutil.rmtree failing on Windows when
875
# readonly files are encountered
876
mutter("hit exception in destroying ScratchBranch: %s" % e)
877
for root, dirs, files in os.walk(self.base, topdown=False):
879
os.chmod(os.path.join(root, name), 0700)
880
shutil.rmtree(self.base)
885
######################################################################
889
def is_control_file(filename):
890
## FIXME: better check
891
filename = os.path.normpath(filename)
892
while filename != '':
893
head, tail = os.path.split(filename)
894
## mutter('check %r for control file' % ((head, tail), ))
895
if tail == bzrlib.BZRDIR:
904
def gen_file_id(name):
905
"""Return new file id.
907
This should probably generate proper UUIDs, but for the moment we
908
cope with just randomness because running uuidgen every time is
913
idx = name.rfind('/')
915
name = name[idx+1 : ]
916
idx = name.rfind('\\')
918
name = name[idx+1 : ]
920
# make it not a hidden file
921
name = name.lstrip('.')
923
# remove any wierd characters; we don't escape them but rather
925
name = re.sub(r'[^\w.]', '', name)
927
s = hexlify(rand_bytes(8))
928
return '-'.join((name, compact_date(time.time()), s))