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.
132
def __init__(self, base, init=False, find_root=True):
133
"""Create new branch object at a particular location.
135
base -- Base directory for the branch.
137
init -- If True, create new control files in a previously
138
unversioned directory. If False, the branch must already
141
find_root -- If true and init is false, find the root of the
142
existing branch containing base.
144
In the test suite, creation of new trees is tested using the
145
`ScratchBranch` class.
148
self.base = os.path.realpath(base)
151
self.base = find_branch_root(base)
153
self.base = os.path.realpath(base)
154
if not isdir(self.controlfilename('.')):
155
from errors import NotBranchError
156
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
157
['use "bzr init" to initialize a new working tree',
158
'current bzr can only operate from top-of-tree'])
160
self._lockfile = self.controlfile('branch-lock', 'wb')
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)
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 lock, LOCK_EX
192
lock(self._lockfile, LOCK_EX)
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 lock, LOCK_SH
206
lock(self._lockfile, LOCK_SH)
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
220
assert self._lock_count == 1
221
from bzrlib.lock import unlock
222
unlock(self._lockfile)
223
self._lock_mode = self._lock_count = None
226
def abspath(self, name):
227
"""Return absolute filename for something in the branch"""
228
return os.path.join(self.base, name)
231
def relpath(self, path):
232
"""Return path relative to this branch of something inside it.
234
Raises an error if path is not in this branch."""
235
return _relpath(self.base, path)
238
def controlfilename(self, file_or_path):
239
"""Return location relative to branch."""
240
if isinstance(file_or_path, types.StringTypes):
241
file_or_path = [file_or_path]
242
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
245
def controlfile(self, file_or_path, mode='r'):
246
"""Open a control file for this branch.
248
There are two classes of file in the control directory: text
249
and binary. binary files are untranslated byte streams. Text
250
control files are stored with Unix newlines and in UTF-8, even
251
if the platform or locale defaults are different.
253
Controlfiles should almost never be opened in write mode but
254
rather should be atomically copied and replaced using atomicfile.
257
fn = self.controlfilename(file_or_path)
259
if mode == 'rb' or mode == 'wb':
260
return file(fn, mode)
261
elif mode == 'r' or mode == 'w':
262
# open in binary mode anyhow so there's no newline translation;
263
# codecs uses line buffering by default; don't want that.
265
return codecs.open(fn, mode + 'b', 'utf-8',
268
raise BzrError("invalid controlfile mode %r" % mode)
272
def _make_control(self):
273
os.mkdir(self.controlfilename([]))
274
self.controlfile('README', 'w').write(
275
"This is a Bazaar-NG control directory.\n"
276
"Do not change any files in this directory.")
277
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
278
for d in ('text-store', 'inventory-store', 'revision-store'):
279
os.mkdir(self.controlfilename(d))
280
for f in ('revision-history', 'merged-patches',
281
'pending-merged-patches', 'branch-name',
283
self.controlfile(f, 'w').write('')
284
mutter('created control directory in ' + self.base)
285
Inventory().write_xml(self.controlfile('inventory','w'))
288
def _check_format(self):
289
"""Check this branch format is supported.
291
The current tool only supports the current unstable format.
293
In the future, we might need different in-memory Branch
294
classes to support downlevel branches. But not yet.
296
# This ignores newlines so that we can open branches created
297
# on Windows from Linux and so on. I think it might be better
298
# to always make all internal files in unix format.
299
fmt = self.controlfile('branch-format', 'r').read()
300
fmt.replace('\r\n', '')
301
if fmt != BZR_BRANCH_FORMAT:
302
raise BzrError('sorry, branch format %r not supported' % fmt,
303
['use a different bzr version',
304
'or remove the .bzr directory and "bzr init" again'])
308
def read_working_inventory(self):
309
"""Read the working inventory."""
311
# ElementTree does its own conversion from UTF-8, so open in
315
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
316
mutter("loaded inventory of %d items in %f"
317
% (len(inv), time.time() - before))
323
def _write_inventory(self, inv):
324
"""Update the working inventory.
326
That is to say, the inventory describing changes underway, that
327
will be committed to the next revision.
329
## TODO: factor out to atomicfile? is rename safe on windows?
330
## TODO: Maybe some kind of clean/dirty marker on inventory?
331
tmpfname = self.controlfilename('inventory.tmp')
332
tmpf = file(tmpfname, 'wb')
335
inv_fname = self.controlfilename('inventory')
336
if sys.platform == 'win32':
338
os.rename(tmpfname, inv_fname)
339
mutter('wrote working inventory')
342
inventory = property(read_working_inventory, _write_inventory, None,
343
"""Inventory for the working copy.""")
346
def add(self, files, verbose=False, ids=None):
347
"""Make files versioned.
349
Note that the command line normally calls smart_add instead.
351
This puts the files in the Added state, so that they will be
352
recorded by the next commit.
355
List of paths to add, relative to the base of the tree.
358
If set, use these instead of automatically generated ids.
359
Must be the same length as the list of files, but may
360
contain None for ids that are to be autogenerated.
362
TODO: Perhaps have an option to add the ids even if the files do
365
TODO: Perhaps return the ids of the files? But then again it
366
is easy to retrieve them if they're needed.
368
TODO: Adding a directory should optionally recurse down and
369
add all non-ignored children. Perhaps do that in a
372
# TODO: Re-adding a file that is removed in the working copy
373
# should probably put it back with the previous ID.
374
if isinstance(files, types.StringTypes):
375
assert(ids is None or isinstance(ids, types.StringTypes))
381
ids = [None] * len(files)
383
assert(len(ids) == len(files))
387
inv = self.read_working_inventory()
388
for f,file_id in zip(files, ids):
389
if is_control_file(f):
390
raise BzrError("cannot add control file %s" % quotefn(f))
395
raise BzrError("cannot add top-level %r" % f)
397
fullpath = os.path.normpath(self.abspath(f))
400
kind = file_kind(fullpath)
402
# maybe something better?
403
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
405
if kind != 'file' and kind != 'directory':
406
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
409
file_id = gen_file_id(f)
410
inv.add_path(f, kind=kind, file_id=file_id)
413
show_status('A', kind, quotefn(f))
415
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
417
self._write_inventory(inv)
422
def print_file(self, file, revno):
423
"""Print `file` to stdout."""
426
tree = self.revision_tree(self.lookup_revision(revno))
427
# use inventory as it was in that revision
428
file_id = tree.inventory.path2id(file)
430
raise BzrError("%r is not present in revision %d" % (file, revno))
431
tree.print_file(file_id)
436
def remove(self, files, verbose=False):
437
"""Mark nominated files for removal from the inventory.
439
This does not remove their text. This does not run on
441
TODO: Refuse to remove modified files unless --force is given?
443
TODO: Do something useful with directories.
445
TODO: Should this remove the text or not? Tough call; not
446
removing may be useful and the user can just use use rm, and
447
is the opposite of add. Removing it is consistent with most
448
other tools. Maybe an option.
450
## TODO: Normalize names
451
## TODO: Remove nested loops; better scalability
452
if isinstance(files, types.StringTypes):
458
tree = self.working_tree()
461
# do this before any modifications
465
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
466
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
468
# having remove it, it must be either ignored or unknown
469
if tree.is_ignored(f):
473
show_status(new_status, inv[fid].kind, quotefn(f))
476
self._write_inventory(inv)
481
# FIXME: this doesn't need to be a branch method
482
def set_inventory(self, new_inventory_list):
484
for path, file_id, parent, kind in new_inventory_list:
485
name = os.path.basename(path)
488
inv.add(InventoryEntry(file_id, name, kind, parent))
489
self._write_inventory(inv)
493
"""Return all unknown files.
495
These are files in the working directory that are not versioned or
496
control files or ignored.
498
>>> b = ScratchBranch(files=['foo', 'foo~'])
499
>>> list(b.unknowns())
502
>>> list(b.unknowns())
505
>>> list(b.unknowns())
508
return self.working_tree().unknowns()
511
def append_revision(self, revision_id):
512
mutter("add {%s} to revision-history" % revision_id)
513
rev_history = self.revision_history()
515
tmprhname = self.controlfilename('revision-history.tmp')
516
rhname = self.controlfilename('revision-history')
518
f = file(tmprhname, 'wt')
519
rev_history.append(revision_id)
520
f.write('\n'.join(rev_history))
524
if sys.platform == 'win32':
526
os.rename(tmprhname, rhname)
530
def get_revision(self, revision_id):
531
"""Return the Revision object for a named revision"""
532
r = Revision.read_xml(self.revision_store[revision_id])
533
assert r.revision_id == revision_id
537
def get_inventory(self, inventory_id):
538
"""Get Inventory object by hash.
540
TODO: Perhaps for this and similar methods, take a revision
541
parameter which can be either an integer revno or a
543
i = Inventory.read_xml(self.inventory_store[inventory_id])
547
def get_revision_inventory(self, revision_id):
548
"""Return inventory of a past revision."""
549
if revision_id == None:
552
return self.get_inventory(self.get_revision(revision_id).inventory_id)
555
def revision_history(self):
556
"""Return sequence of revision hashes on to this branch.
558
>>> ScratchBranch().revision_history()
563
return [l.rstrip('\r\n') for l in
564
self.controlfile('revision-history', 'r').readlines()]
569
def enum_history(self, direction):
570
"""Return (revno, revision_id) for history of branch.
573
'forward' is from earliest to latest
574
'reverse' is from latest to earliest
576
rh = self.revision_history()
577
if direction == 'forward':
582
elif direction == 'reverse':
588
raise ValueError('invalid history direction', direction)
592
"""Return current revision number for this branch.
594
That is equivalent to the number of revisions committed to
597
return len(self.revision_history())
600
def last_patch(self):
601
"""Return last patch hash, or None if no history.
603
ph = self.revision_history()
610
def commit(self, *args, **kw):
612
from bzrlib.commit import commit
613
commit(self, *args, **kw)
616
def lookup_revision(self, revno):
617
"""Return revision hash for revision number."""
622
# list is 0-based; revisions are 1-based
623
return self.revision_history()[revno-1]
625
raise BzrError("no such revision %s" % revno)
628
def revision_tree(self, revision_id):
629
"""Return Tree for a revision on this branch.
631
`revision_id` may be None for the null revision, in which case
632
an `EmptyTree` is returned."""
633
# TODO: refactor this to use an existing revision object
634
# so we don't need to read it in twice.
635
if revision_id == None:
638
inv = self.get_revision_inventory(revision_id)
639
return RevisionTree(self.text_store, inv)
642
def working_tree(self):
643
"""Return a `Tree` for the working copy."""
644
from workingtree import WorkingTree
645
return WorkingTree(self.base, self.read_working_inventory())
648
def basis_tree(self):
649
"""Return `Tree` object for last revision.
651
If there are no revisions yet, return an `EmptyTree`.
653
r = self.last_patch()
657
return RevisionTree(self.text_store, self.get_revision_inventory(r))
661
def rename_one(self, from_rel, to_rel):
664
This can change the directory or the filename or both.
668
tree = self.working_tree()
670
if not tree.has_filename(from_rel):
671
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
672
if tree.has_filename(to_rel):
673
raise BzrError("can't rename: new working file %r already exists" % to_rel)
675
file_id = inv.path2id(from_rel)
677
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
679
if inv.path2id(to_rel):
680
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
682
to_dir, to_tail = os.path.split(to_rel)
683
to_dir_id = inv.path2id(to_dir)
684
if to_dir_id == None and to_dir != '':
685
raise BzrError("can't determine destination directory id for %r" % to_dir)
687
mutter("rename_one:")
688
mutter(" file_id {%s}" % file_id)
689
mutter(" from_rel %r" % from_rel)
690
mutter(" to_rel %r" % to_rel)
691
mutter(" to_dir %r" % to_dir)
692
mutter(" to_dir_id {%s}" % to_dir_id)
694
inv.rename(file_id, to_dir_id, to_tail)
696
print "%s => %s" % (from_rel, to_rel)
698
from_abs = self.abspath(from_rel)
699
to_abs = self.abspath(to_rel)
701
os.rename(from_abs, to_abs)
703
raise BzrError("failed to rename %r to %r: %s"
704
% (from_abs, to_abs, e[1]),
705
["rename rolled back"])
707
self._write_inventory(inv)
712
def move(self, from_paths, to_name):
715
to_name must exist as a versioned directory.
717
If to_name exists and is a directory, the files are moved into
718
it, keeping their old names. If it is a directory,
720
Note that to_name is only the last component of the new name;
721
this doesn't change the directory.
725
## TODO: Option to move IDs only
726
assert not isinstance(from_paths, basestring)
727
tree = self.working_tree()
729
to_abs = self.abspath(to_name)
730
if not isdir(to_abs):
731
raise BzrError("destination %r is not a directory" % to_abs)
732
if not tree.has_filename(to_name):
733
raise BzrError("destination %r not in working directory" % to_abs)
734
to_dir_id = inv.path2id(to_name)
735
if to_dir_id == None and to_name != '':
736
raise BzrError("destination %r is not a versioned directory" % to_name)
737
to_dir_ie = inv[to_dir_id]
738
if to_dir_ie.kind not in ('directory', 'root_directory'):
739
raise BzrError("destination %r is not a directory" % to_abs)
741
to_idpath = inv.get_idpath(to_dir_id)
744
if not tree.has_filename(f):
745
raise BzrError("%r does not exist in working tree" % f)
746
f_id = inv.path2id(f)
748
raise BzrError("%r is not versioned" % f)
749
name_tail = splitpath(f)[-1]
750
dest_path = appendpath(to_name, name_tail)
751
if tree.has_filename(dest_path):
752
raise BzrError("destination %r already exists" % dest_path)
753
if f_id in to_idpath:
754
raise BzrError("can't move %r to a subdirectory of itself" % f)
756
# OK, so there's a race here, it's possible that someone will
757
# create a file in this interval and then the rename might be
758
# left half-done. But we should have caught most problems.
761
name_tail = splitpath(f)[-1]
762
dest_path = appendpath(to_name, name_tail)
763
print "%s => %s" % (f, dest_path)
764
inv.rename(inv.path2id(f), to_dir_id, name_tail)
766
os.rename(self.abspath(f), self.abspath(dest_path))
768
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
769
["rename rolled back"])
771
self._write_inventory(inv)
777
class ScratchBranch(Branch):
778
"""Special test class: a branch that cleans up after itself.
780
>>> b = ScratchBranch()
788
def __init__(self, files=[], dirs=[]):
789
"""Make a test branch.
791
This creates a temporary directory and runs init-tree in it.
793
If any files are listed, they are created in the working copy.
795
Branch.__init__(self, tempfile.mkdtemp(), init=True)
797
os.mkdir(self.abspath(d))
800
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
807
"""Destroy the test branch, removing the scratch directory."""
810
mutter("delete ScratchBranch %s" % self.base)
811
shutil.rmtree(self.base)
813
# Work around for shutil.rmtree failing on Windows when
814
# readonly files are encountered
815
mutter("hit exception in destroying ScratchBranch: %s" % e)
816
for root, dirs, files in os.walk(self.base, topdown=False):
818
os.chmod(os.path.join(root, name), 0700)
819
shutil.rmtree(self.base)
824
######################################################################
828
def is_control_file(filename):
829
## FIXME: better check
830
filename = os.path.normpath(filename)
831
while filename != '':
832
head, tail = os.path.split(filename)
833
## mutter('check %r for control file' % ((head, tail), ))
834
if tail == bzrlib.BZRDIR:
843
def gen_file_id(name):
844
"""Return new file id.
846
This should probably generate proper UUIDs, but for the moment we
847
cope with just randomness because running uuidgen every time is
852
idx = name.rfind('/')
854
name = name[idx+1 : ]
855
idx = name.rfind('\\')
857
name = name[idx+1 : ]
859
# make it not a hidden file
860
name = name.lstrip('.')
862
# remove any wierd characters; we don't escape them but rather
864
name = re.sub(r'[^\w.]', '', name)
866
s = hexlify(rand_bytes(8))
867
return '-'.join((name, compact_date(time.time()), s))