15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
import traceback, socket, fnmatch, difflib, time
22
from binascii import hexlify
25
from inventory import Inventory
26
from trace import mutter, note
27
from tree import Tree, EmptyTree, RevisionTree
28
from inventory import InventoryEntry, Inventory
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
32
from store import ImmutableStore
33
from revision import Revision
34
from errors import bailout, BzrError
35
from textui import show_status
22
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
rename, splitpath, sha_file, appendpath, file_kind
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
27
DivergedBranches, NotBranchError
28
from bzrlib.textui import show_status
29
from bzrlib.revision import Revision
30
from bzrlib.delta import compare_trees
31
from bzrlib.tree import EmptyTree, RevisionTree
37
37
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
38
38
## TODO: Maybe include checks for common corruption of newlines, etc?
42
def find_branch(f, **args):
43
if f and (f.startswith('http://') or f.startswith('https://')):
45
return remotebranch.RemoteBranch(f, **args)
41
# TODO: Some operations like log might retrieve the same revisions
42
# repeatedly to calculate deltas. We could perhaps have a weakref
43
# cache in memory to make this faster.
45
def find_branch(*ignored, **ignored_too):
46
# XXX: leave this here for about one release, then remove it
47
raise NotImplementedError('find_branch() is not supported anymore, '
48
'please use one of the new branch constructors')
50
def _relpath(base, path):
51
"""Return path relative to base, or raise exception.
53
The path may be either an absolute path or a path relative to the
54
current working directory.
56
Lifted out of Branch.relpath for ease of testing.
58
os.path.commonprefix (python2.4) has a bad bug that it works just
59
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
60
avoids that problem."""
61
rp = os.path.abspath(path)
65
while len(head) >= len(base):
68
head, tail = os.path.split(head)
47
return Branch(f, **args)
72
raise NotBranchError("path %r is not within branch %r" % (rp, base))
50
77
def find_branch_root(f=None):
74
102
head, tail = os.path.split(f)
76
104
# reached the root, whatever that may be
77
raise BzrError('%r is not in a branch' % orig_f)
105
raise NotBranchError('%s is not in a branch' % orig_f)
82
111
######################################################################
114
class Branch(object):
86
115
"""Branch holding a history of revisions.
89
Base directory of the branch.
93
def __init__(self, base, init=False, find_root=True, lock_mode='w'):
118
Base directory/url of the branch.
122
def __init__(self, *ignored, **ignored_too):
123
raise NotImplementedError('The Branch class is abstract')
127
"""Open an existing branch, rooted at 'base' (url)"""
128
if base and (base.startswith('http://') or base.startswith('https://')):
129
from bzrlib.remotebranch import RemoteBranch
130
return RemoteBranch(base, find_root=False)
132
return LocalBranch(base, find_root=False)
135
def open_containing(url):
136
"""Open an existing branch which contains url.
138
This probes for a branch at url, and searches upwards from there.
140
if url and (url.startswith('http://') or url.startswith('https://')):
141
from bzrlib.remotebranch import RemoteBranch
142
return RemoteBranch(url)
144
return LocalBranch(url)
147
def initialize(base):
148
"""Create a new branch, rooted at 'base' (url)"""
149
if base and (base.startswith('http://') or base.startswith('https://')):
150
from bzrlib.remotebranch import RemoteBranch
151
return RemoteBranch(base, init=True)
153
return LocalBranch(base, init=True)
155
def setup_caching(self, cache_root):
156
"""Subclasses that care about caching should override this, and set
157
up cached stores located under cache_root.
161
class LocalBranch(Branch):
162
"""A branch stored in the actual filesystem.
164
Note that it's "local" in the context of the filesystem; it doesn't
165
really matter if it's on an nfs/smb/afs/coda/... share, as long as
166
it's writable, and can be accessed via the normal filesystem API.
172
If _lock_mode is true, a positive count of the number of times the
176
Lock object from bzrlib.lock.
178
# We actually expect this class to be somewhat short-lived; part of its
179
# purpose is to try to isolate what bits of the branch logic are tied to
180
# filesystem access, so that in a later step, we can extricate them to
181
# a separarte ("storage") class.
186
def __init__(self, base, init=False, find_root=True):
94
187
"""Create new branch object at a particular location.
96
base -- Base directory for the branch.
189
base -- Base directory for the branch. May be a file:// url.
98
191
init -- If True, create new control files in a previously
99
192
unversioned directory. If False, the branch must already
131
226
__repr__ = __str__
135
def lock(self, mode='w'):
136
"""Lock the on-disk branch, excluding other processes."""
142
om = os.O_WRONLY | os.O_CREAT
147
raise BzrError("invalid locking mode %r" % mode)
150
lockfile = os.open(self.controlfilename('branch-lock'), om)
152
if e.errno == errno.ENOENT:
153
# might not exist on branches from <0.0.4
154
self.controlfile('branch-lock', 'w').close()
155
lockfile = os.open(self.controlfilename('branch-lock'), om)
159
fcntl.lockf(lockfile, lm)
161
fcntl.lockf(lockfile, fcntl.LOCK_UN)
163
self._lockmode = None
165
self._lockmode = mode
167
warning("please write a locking method for platform %r" % sys.platform)
169
self._lockmode = None
171
self._lockmode = mode
174
def _need_readlock(self):
175
if self._lockmode not in ['r', 'w']:
176
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
178
def _need_writelock(self):
179
if self._lockmode not in ['w']:
180
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
230
if self._lock_mode or self._lock:
231
from bzrlib.warnings import warn
232
warn("branch %r was not explicitly unlocked" % self)
235
def lock_write(self):
237
if self._lock_mode != 'w':
238
from bzrlib.errors import LockError
239
raise LockError("can't upgrade to a write lock from %r" %
241
self._lock_count += 1
243
from bzrlib.lock import WriteLock
245
self._lock = WriteLock(self.controlfilename('branch-lock'))
246
self._lock_mode = 'w'
252
assert self._lock_mode in ('r', 'w'), \
253
"invalid lock mode %r" % self._lock_mode
254
self._lock_count += 1
256
from bzrlib.lock import ReadLock
258
self._lock = ReadLock(self.controlfilename('branch-lock'))
259
self._lock_mode = 'r'
263
if not self._lock_mode:
264
from bzrlib.errors import LockError
265
raise LockError('branch %r is not locked' % (self))
267
if self._lock_count > 1:
268
self._lock_count -= 1
272
self._lock_mode = self._lock_count = None
183
274
def abspath(self, name):
184
275
"""Return absolute filename for something in the branch"""
185
276
return os.path.join(self.base, name)
188
278
def relpath(self, path):
189
279
"""Return path relative to this branch of something inside it.
191
281
Raises an error if path is not in this branch."""
192
rp = os.path.realpath(path)
194
if not rp.startswith(self.base):
195
bailout("path %r is not within branch %r" % (rp, self.base))
196
rp = rp[len(self.base):]
197
rp = rp.lstrip(os.sep)
282
return _relpath(self.base, path)
201
284
def controlfilename(self, file_or_path):
202
285
"""Return location relative to branch."""
203
if isinstance(file_or_path, types.StringTypes):
286
if isinstance(file_or_path, basestring):
204
287
file_or_path = [file_or_path]
205
288
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
260
349
# on Windows from Linux and so on. I think it might be better
261
350
# to always make all internal files in unix format.
262
351
fmt = self.controlfile('branch-format', 'r').read()
263
fmt.replace('\r\n', '')
352
fmt = fmt.replace('\r\n', '\n')
264
353
if fmt != BZR_BRANCH_FORMAT:
265
bailout('sorry, branch format %r not supported' % fmt,
266
['use a different bzr version',
267
'or remove the .bzr directory and "bzr init" again'])
354
raise BzrError('sorry, branch format %r not supported' % fmt,
355
['use a different bzr version',
356
'or remove the .bzr directory and "bzr init" again'])
358
def get_root_id(self):
359
"""Return the id of this branches root"""
360
inv = self.read_working_inventory()
361
return inv.root.file_id
363
def set_root_id(self, file_id):
364
inv = self.read_working_inventory()
365
orig_root_id = inv.root.file_id
366
del inv._byid[inv.root.file_id]
367
inv.root.file_id = file_id
368
inv._byid[inv.root.file_id] = inv.root
371
if entry.parent_id in (None, orig_root_id):
372
entry.parent_id = inv.root.file_id
373
self._write_inventory(inv)
270
375
def read_working_inventory(self):
271
376
"""Read the working inventory."""
272
self._need_readlock()
274
# ElementTree does its own conversion from UTF-8, so open in
276
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
277
mutter("loaded inventory of %d items in %f"
278
% (len(inv), time.time() - before))
377
from bzrlib.inventory import Inventory
380
# ElementTree does its own conversion from UTF-8, so open in
382
f = self.controlfile('inventory', 'rb')
383
return bzrlib.xml.serializer_v4.read_inventory(f)
282
388
def _write_inventory(self, inv):
283
389
"""Update the working inventory.
285
391
That is to say, the inventory describing changes underway, that
286
392
will be committed to the next revision.
288
self._need_writelock()
289
## TODO: factor out to atomicfile? is rename safe on windows?
290
## TODO: Maybe some kind of clean/dirty marker on inventory?
291
tmpfname = self.controlfilename('inventory.tmp')
292
tmpf = file(tmpfname, 'wb')
295
inv_fname = self.controlfilename('inventory')
296
if sys.platform == 'win32':
298
os.rename(tmpfname, inv_fname)
394
from bzrlib.atomicfile import AtomicFile
398
f = AtomicFile(self.controlfilename('inventory'), 'wb')
400
bzrlib.xml.serializer_v4.write_inventory(inv, f)
299
407
mutter('wrote working inventory')
302
410
inventory = property(read_working_inventory, _write_inventory, None,
303
411
"""Inventory for the working copy.""")
306
def add(self, files, verbose=False, ids=None):
414
def add(self, files, ids=None):
307
415
"""Make files versioned.
309
Note that the command line normally calls smart_add instead.
417
Note that the command line normally calls smart_add instead,
418
which can automatically recurse.
311
420
This puts the files in the Added state, so that they will be
312
421
recorded by the next commit.
424
List of paths to add, relative to the base of the tree.
427
If set, use these instead of automatically generated ids.
428
Must be the same length as the list of files, but may
429
contain None for ids that are to be autogenerated.
314
431
TODO: Perhaps have an option to add the ids even if the files do
317
TODO: Perhaps return the ids of the files? But then again it
318
is easy to retrieve them if they're needed.
320
TODO: Option to specify file id.
322
TODO: Adding a directory should optionally recurse down and
323
add all non-ignored children. Perhaps do that in a
434
TODO: Perhaps yield the ids and paths as they're added.
326
self._need_writelock()
328
436
# TODO: Re-adding a file that is removed in the working copy
329
437
# should probably put it back with the previous ID.
330
if isinstance(files, types.StringTypes):
331
assert(ids is None or isinstance(ids, types.StringTypes))
438
if isinstance(files, basestring):
439
assert(ids is None or isinstance(ids, basestring))
333
441
if ids is not None:
337
445
ids = [None] * len(files)
339
447
assert(len(ids) == len(files))
341
inv = self.read_working_inventory()
342
for f,file_id in zip(files, ids):
343
if is_control_file(f):
344
bailout("cannot add control file %s" % quotefn(f))
349
bailout("cannot add top-level %r" % f)
351
fullpath = os.path.normpath(self.abspath(f))
354
kind = file_kind(fullpath)
356
# maybe something better?
357
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
359
if kind != 'file' and kind != 'directory':
360
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
363
file_id = gen_file_id(f)
364
inv.add_path(f, kind=kind, file_id=file_id)
367
show_status('A', kind, quotefn(f))
369
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
371
self._write_inventory(inv)
451
inv = self.read_working_inventory()
452
for f,file_id in zip(files, ids):
453
if is_control_file(f):
454
raise BzrError("cannot add control file %s" % quotefn(f))
459
raise BzrError("cannot add top-level %r" % f)
461
fullpath = os.path.normpath(self.abspath(f))
464
kind = file_kind(fullpath)
466
# maybe something better?
467
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
469
if kind != 'file' and kind != 'directory':
470
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
473
file_id = gen_file_id(f)
474
inv.add_path(f, kind=kind, file_id=file_id)
476
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
478
self._write_inventory(inv)
374
483
def print_file(self, file, revno):
375
484
"""Print `file` to stdout."""
376
self._need_readlock()
377
tree = self.revision_tree(self.lookup_revision(revno))
378
# use inventory as it was in that revision
379
file_id = tree.inventory.path2id(file)
381
bailout("%r is not present in revision %d" % (file, revno))
382
tree.print_file(file_id)
487
tree = self.revision_tree(self.get_rev_id(revno))
488
# use inventory as it was in that revision
489
file_id = tree.inventory.path2id(file)
491
raise BzrError("%r is not present in revision %s" % (file, revno))
492
tree.print_file(file_id)
385
497
def remove(self, files, verbose=False):
386
498
"""Mark nominated files for removal from the inventory.
399
511
## TODO: Normalize names
400
512
## TODO: Remove nested loops; better scalability
401
self._need_writelock()
403
if isinstance(files, types.StringTypes):
513
if isinstance(files, basestring):
406
tree = self.working_tree()
409
# do this before any modifications
413
bailout("cannot remove unversioned file %s" % quotefn(f))
414
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
416
# having remove it, it must be either ignored or unknown
417
if tree.is_ignored(f):
421
show_status(new_status, inv[fid].kind, quotefn(f))
424
self._write_inventory(inv)
519
tree = self.working_tree()
522
# do this before any modifications
526
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
527
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
529
# having remove it, it must be either ignored or unknown
530
if tree.is_ignored(f):
534
show_status(new_status, inv[fid].kind, quotefn(f))
537
self._write_inventory(inv)
542
# FIXME: this doesn't need to be a branch method
426
543
def set_inventory(self, new_inventory_list):
544
from bzrlib.inventory import Inventory, InventoryEntry
545
inv = Inventory(self.get_root_id())
428
546
for path, file_id, parent, kind in new_inventory_list:
429
547
name = os.path.basename(path)
452
570
return self.working_tree().unknowns()
455
def append_revision(self, revision_id):
456
mutter("add {%s} to revision-history" % revision_id)
573
def append_revision(self, *revision_ids):
574
from bzrlib.atomicfile import AtomicFile
576
for revision_id in revision_ids:
577
mutter("add {%s} to revision-history" % revision_id)
457
579
rev_history = self.revision_history()
459
tmprhname = self.controlfilename('revision-history.tmp')
460
rhname = self.controlfilename('revision-history')
462
f = file(tmprhname, 'wt')
463
rev_history.append(revision_id)
464
f.write('\n'.join(rev_history))
468
if sys.platform == 'win32':
470
os.rename(tmprhname, rhname)
580
rev_history.extend(revision_ids)
582
f = AtomicFile(self.controlfilename('revision-history'))
584
for rev_id in rev_history:
591
def get_revision_xml_file(self, revision_id):
592
"""Return XML file object for revision object."""
593
if not revision_id or not isinstance(revision_id, basestring):
594
raise InvalidRevisionId(revision_id)
599
return self.revision_store[revision_id]
600
except (IndexError, KeyError):
601
raise bzrlib.errors.NoSuchRevision(self, revision_id)
607
get_revision_xml = get_revision_xml_file
474
610
def get_revision(self, revision_id):
475
611
"""Return the Revision object for a named revision"""
476
self._need_readlock()
477
r = Revision.read_xml(self.revision_store[revision_id])
612
xml_file = self.get_revision_xml_file(revision_id)
615
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
616
except SyntaxError, e:
617
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
478
621
assert r.revision_id == revision_id
625
def get_revision_delta(self, revno):
626
"""Return the delta for one revision.
628
The delta is relative to its mainline predecessor, or the
629
empty tree for revision 1.
631
assert isinstance(revno, int)
632
rh = self.revision_history()
633
if not (1 <= revno <= len(rh)):
634
raise InvalidRevisionNumber(revno)
636
# revno is 1-based; list is 0-based
638
new_tree = self.revision_tree(rh[revno-1])
640
old_tree = EmptyTree()
642
old_tree = self.revision_tree(rh[revno-2])
644
return compare_trees(old_tree, new_tree)
648
def get_revision_sha1(self, revision_id):
649
"""Hash the stored value of a revision, and return it."""
650
# In the future, revision entries will be signed. At that
651
# point, it is probably best *not* to include the signature
652
# in the revision hash. Because that lets you re-sign
653
# the revision, (add signatures/remove signatures) and still
654
# have all hash pointers stay consistent.
655
# But for now, just hash the contents.
656
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
482
659
def get_inventory(self, inventory_id):
483
660
"""Get Inventory object by hash.
485
662
TODO: Perhaps for this and similar methods, take a revision
486
663
parameter which can be either an integer revno or a
488
self._need_readlock()
489
i = Inventory.read_xml(self.inventory_store[inventory_id])
665
from bzrlib.inventory import Inventory
667
f = self.get_inventory_xml_file(inventory_id)
668
return bzrlib.xml.serializer_v4.read_inventory(f)
671
def get_inventory_xml(self, inventory_id):
672
"""Get inventory XML as a file object."""
673
return self.inventory_store[inventory_id]
675
get_inventory_xml_file = get_inventory_xml
678
def get_inventory_sha1(self, inventory_id):
679
"""Return the sha1 hash of the inventory entry
681
return sha_file(self.get_inventory_xml(inventory_id))
493
684
def get_revision_inventory(self, revision_id):
494
685
"""Return inventory of a past revision."""
495
self._need_readlock()
686
# bzr 0.0.6 imposes the constraint that the inventory_id
687
# must be the same as its revision, so this is trivial.
496
688
if revision_id == None:
689
from bzrlib.inventory import Inventory
690
return Inventory(self.get_root_id())
499
return self.get_inventory(self.get_revision(revision_id).inventory_id)
692
return self.get_inventory(revision_id)
502
695
def revision_history(self):
505
698
>>> ScratchBranch().revision_history()
508
self._need_readlock()
509
return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
512
def enum_history(self, direction):
513
"""Return (revno, revision_id) for history of branch.
516
'forward' is from earliest to latest
517
'reverse' is from latest to earliest
519
rh = self.revision_history()
520
if direction == 'forward':
525
elif direction == 'reverse':
531
raise BzrError('invalid history direction %r' % direction)
703
return [l.rstrip('\r\n') for l in
704
self.controlfile('revision-history', 'r').readlines()]
709
def common_ancestor(self, other, self_revno=None, other_revno=None):
711
>>> from bzrlib.commit import commit
712
>>> sb = ScratchBranch(files=['foo', 'foo~'])
713
>>> sb.common_ancestor(sb) == (None, None)
715
>>> commit(sb, "Committing first revision", verbose=False)
716
>>> sb.common_ancestor(sb)[0]
718
>>> clone = sb.clone()
719
>>> commit(sb, "Committing second revision", verbose=False)
720
>>> sb.common_ancestor(sb)[0]
722
>>> sb.common_ancestor(clone)[0]
724
>>> commit(clone, "Committing divergent second revision",
726
>>> sb.common_ancestor(clone)[0]
728
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
730
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
732
>>> clone2 = sb.clone()
733
>>> sb.common_ancestor(clone2)[0]
735
>>> sb.common_ancestor(clone2, self_revno=1)[0]
737
>>> sb.common_ancestor(clone2, other_revno=1)[0]
740
my_history = self.revision_history()
741
other_history = other.revision_history()
742
if self_revno is None:
743
self_revno = len(my_history)
744
if other_revno is None:
745
other_revno = len(other_history)
746
indices = range(min((self_revno, other_revno)))
749
if my_history[r] == other_history[r]:
750
return r+1, my_history[r]
773
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
775
If self and other have not diverged, return a list of the revisions
776
present in other, but missing from self.
778
>>> from bzrlib.commit import commit
779
>>> bzrlib.trace.silent = True
780
>>> br1 = ScratchBranch()
781
>>> br2 = ScratchBranch()
782
>>> br1.missing_revisions(br2)
784
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
785
>>> br1.missing_revisions(br2)
787
>>> br2.missing_revisions(br1)
789
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
790
>>> br1.missing_revisions(br2)
792
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
793
>>> br1.missing_revisions(br2)
795
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
796
>>> br1.missing_revisions(br2)
797
Traceback (most recent call last):
798
DivergedBranches: These branches have diverged.
800
self_history = self.revision_history()
801
self_len = len(self_history)
802
other_history = other.revision_history()
803
other_len = len(other_history)
804
common_index = min(self_len, other_len) -1
805
if common_index >= 0 and \
806
self_history[common_index] != other_history[common_index]:
807
raise DivergedBranches(self, other)
809
if stop_revision is None:
810
stop_revision = other_len
811
elif stop_revision > other_len:
812
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
814
return other_history[self_len:stop_revision]
817
def update_revisions(self, other, stop_revision=None):
818
"""Pull in all new revisions from other branch.
820
from bzrlib.fetch import greedy_fetch
821
from bzrlib.revision import get_intervening_revisions
823
pb = bzrlib.ui.ui_factory.progress_bar()
824
pb.update('comparing histories')
825
if stop_revision is None:
826
other_revision = other.last_patch()
828
other_revision = other.get_rev_id(stop_revision)
829
count = greedy_fetch(self, other, other_revision, pb)[0]
831
revision_ids = self.missing_revisions(other, stop_revision)
832
except DivergedBranches, e:
834
revision_ids = get_intervening_revisions(self.last_patch(),
835
other_revision, self)
836
assert self.last_patch() not in revision_ids
837
except bzrlib.errors.NotAncestor:
840
self.append_revision(*revision_ids)
843
def install_revisions(self, other, revision_ids, pb):
844
if hasattr(other.revision_store, "prefetch"):
845
other.revision_store.prefetch(revision_ids)
846
if hasattr(other.inventory_store, "prefetch"):
848
for rev_id in revision_ids:
850
revision = other.get_revision(rev_id).inventory_id
851
inventory_ids.append(revision)
852
except bzrlib.errors.NoSuchRevision:
854
other.inventory_store.prefetch(inventory_ids)
857
pb = bzrlib.ui.ui_factory.progress_bar()
864
for i, rev_id in enumerate(revision_ids):
865
pb.update('fetching revision', i+1, len(revision_ids))
867
rev = other.get_revision(rev_id)
868
except bzrlib.errors.NoSuchRevision:
872
revisions.append(rev)
873
inv = other.get_inventory(str(rev.inventory_id))
874
for key, entry in inv.iter_entries():
875
if entry.text_id is None:
877
if entry.text_id not in self.text_store:
878
needed_texts.add(entry.text_id)
882
count, cp_fail = self.text_store.copy_multi(other.text_store,
884
#print "Added %d texts." % count
885
inventory_ids = [ f.inventory_id for f in revisions ]
886
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
888
#print "Added %d inventories." % count
889
revision_ids = [ f.revision_id for f in revisions]
891
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
894
assert len(cp_fail) == 0
895
return count, failures
553
898
def commit(self, *args, **kw):
555
899
from bzrlib.commit import commit
556
900
commit(self, *args, **kw)
902
def revision_id_to_revno(self, revision_id):
903
"""Given a revision id, return its revno"""
904
history = self.revision_history()
906
return history.index(revision_id) + 1
908
raise bzrlib.errors.NoSuchRevision(self, revision_id)
559
def lookup_revision(self, revno):
560
"""Return revision hash for revision number."""
910
def get_rev_id(self, revno, history=None):
911
"""Find the revision id of the specified revno."""
565
# list is 0-based; revisions are 1-based
566
return self.revision_history()[revno-1]
568
raise BzrError("no such revision %s" % revno)
915
history = self.revision_history()
916
elif revno <= 0 or revno > len(history):
917
raise bzrlib.errors.NoSuchRevision(self, revno)
918
return history[revno - 1]
571
921
def revision_tree(self, revision_id):
606
957
This can change the directory or the filename or both.
608
self._need_writelock()
609
tree = self.working_tree()
611
if not tree.has_filename(from_rel):
612
bailout("can't rename: old working file %r does not exist" % from_rel)
613
if tree.has_filename(to_rel):
614
bailout("can't rename: new working file %r already exists" % to_rel)
616
file_id = inv.path2id(from_rel)
618
bailout("can't rename: old name %r is not versioned" % from_rel)
620
if inv.path2id(to_rel):
621
bailout("can't rename: new name %r is already versioned" % to_rel)
623
to_dir, to_tail = os.path.split(to_rel)
624
to_dir_id = inv.path2id(to_dir)
625
if to_dir_id == None and to_dir != '':
626
bailout("can't determine destination directory id for %r" % to_dir)
628
mutter("rename_one:")
629
mutter(" file_id {%s}" % file_id)
630
mutter(" from_rel %r" % from_rel)
631
mutter(" to_rel %r" % to_rel)
632
mutter(" to_dir %r" % to_dir)
633
mutter(" to_dir_id {%s}" % to_dir_id)
635
inv.rename(file_id, to_dir_id, to_tail)
637
print "%s => %s" % (from_rel, to_rel)
639
from_abs = self.abspath(from_rel)
640
to_abs = self.abspath(to_rel)
642
os.rename(from_abs, to_abs)
644
bailout("failed to rename %r to %r: %s"
645
% (from_abs, to_abs, e[1]),
646
["rename rolled back"])
648
self._write_inventory(inv)
961
tree = self.working_tree()
963
if not tree.has_filename(from_rel):
964
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
965
if tree.has_filename(to_rel):
966
raise BzrError("can't rename: new working file %r already exists" % to_rel)
968
file_id = inv.path2id(from_rel)
970
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
972
if inv.path2id(to_rel):
973
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
975
to_dir, to_tail = os.path.split(to_rel)
976
to_dir_id = inv.path2id(to_dir)
977
if to_dir_id == None and to_dir != '':
978
raise BzrError("can't determine destination directory id for %r" % to_dir)
980
mutter("rename_one:")
981
mutter(" file_id {%s}" % file_id)
982
mutter(" from_rel %r" % from_rel)
983
mutter(" to_rel %r" % to_rel)
984
mutter(" to_dir %r" % to_dir)
985
mutter(" to_dir_id {%s}" % to_dir_id)
987
inv.rename(file_id, to_dir_id, to_tail)
989
from_abs = self.abspath(from_rel)
990
to_abs = self.abspath(to_rel)
992
rename(from_abs, to_abs)
994
raise BzrError("failed to rename %r to %r: %s"
995
% (from_abs, to_abs, e[1]),
996
["rename rolled back"])
998
self._write_inventory(inv)
652
1003
def move(self, from_paths, to_name):
660
1011
Note that to_name is only the last component of the new name;
661
1012
this doesn't change the directory.
663
self._need_writelock()
664
## TODO: Option to move IDs only
665
assert not isinstance(from_paths, basestring)
666
tree = self.working_tree()
668
to_abs = self.abspath(to_name)
669
if not isdir(to_abs):
670
bailout("destination %r is not a directory" % to_abs)
671
if not tree.has_filename(to_name):
672
bailout("destination %r not in working directory" % to_abs)
673
to_dir_id = inv.path2id(to_name)
674
if to_dir_id == None and to_name != '':
675
bailout("destination %r is not a versioned directory" % to_name)
676
to_dir_ie = inv[to_dir_id]
677
if to_dir_ie.kind not in ('directory', 'root_directory'):
678
bailout("destination %r is not a directory" % to_abs)
680
to_idpath = Set(inv.get_idpath(to_dir_id))
683
if not tree.has_filename(f):
684
bailout("%r does not exist in working tree" % f)
685
f_id = inv.path2id(f)
687
bailout("%r is not versioned" % f)
688
name_tail = splitpath(f)[-1]
689
dest_path = appendpath(to_name, name_tail)
690
if tree.has_filename(dest_path):
691
bailout("destination %r already exists" % dest_path)
692
if f_id in to_idpath:
693
bailout("can't move %r to a subdirectory of itself" % f)
695
# OK, so there's a race here, it's possible that someone will
696
# create a file in this interval and then the rename might be
697
# left half-done. But we should have caught most problems.
700
name_tail = splitpath(f)[-1]
701
dest_path = appendpath(to_name, name_tail)
702
print "%s => %s" % (f, dest_path)
703
inv.rename(inv.path2id(f), to_dir_id, name_tail)
705
os.rename(self.abspath(f), self.abspath(dest_path))
707
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
708
["rename rolled back"])
710
self._write_inventory(inv)
715
class ScratchBranch(Branch):
1014
This returns a list of (from_path, to_path) pairs for each
1015
entry that is moved.
1020
## TODO: Option to move IDs only
1021
assert not isinstance(from_paths, basestring)
1022
tree = self.working_tree()
1023
inv = tree.inventory
1024
to_abs = self.abspath(to_name)
1025
if not isdir(to_abs):
1026
raise BzrError("destination %r is not a directory" % to_abs)
1027
if not tree.has_filename(to_name):
1028
raise BzrError("destination %r not in working directory" % to_abs)
1029
to_dir_id = inv.path2id(to_name)
1030
if to_dir_id == None and to_name != '':
1031
raise BzrError("destination %r is not a versioned directory" % to_name)
1032
to_dir_ie = inv[to_dir_id]
1033
if to_dir_ie.kind not in ('directory', 'root_directory'):
1034
raise BzrError("destination %r is not a directory" % to_abs)
1036
to_idpath = inv.get_idpath(to_dir_id)
1038
for f in from_paths:
1039
if not tree.has_filename(f):
1040
raise BzrError("%r does not exist in working tree" % f)
1041
f_id = inv.path2id(f)
1043
raise BzrError("%r is not versioned" % f)
1044
name_tail = splitpath(f)[-1]
1045
dest_path = appendpath(to_name, name_tail)
1046
if tree.has_filename(dest_path):
1047
raise BzrError("destination %r already exists" % dest_path)
1048
if f_id in to_idpath:
1049
raise BzrError("can't move %r to a subdirectory of itself" % f)
1051
# OK, so there's a race here, it's possible that someone will
1052
# create a file in this interval and then the rename might be
1053
# left half-done. But we should have caught most problems.
1055
for f in from_paths:
1056
name_tail = splitpath(f)[-1]
1057
dest_path = appendpath(to_name, name_tail)
1058
result.append((f, dest_path))
1059
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1061
rename(self.abspath(f), self.abspath(dest_path))
1063
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1064
["rename rolled back"])
1066
self._write_inventory(inv)
1073
def revert(self, filenames, old_tree=None, backups=True):
1074
"""Restore selected files to the versions from a previous tree.
1077
If true (default) backups are made of files before
1080
from bzrlib.errors import NotVersionedError, BzrError
1081
from bzrlib.atomicfile import AtomicFile
1082
from bzrlib.osutils import backup_file
1084
inv = self.read_working_inventory()
1085
if old_tree is None:
1086
old_tree = self.basis_tree()
1087
old_inv = old_tree.inventory
1090
for fn in filenames:
1091
file_id = inv.path2id(fn)
1093
raise NotVersionedError("not a versioned file", fn)
1094
if not old_inv.has_id(file_id):
1095
raise BzrError("file not present in old tree", fn, file_id)
1096
nids.append((fn, file_id))
1098
# TODO: Rename back if it was previously at a different location
1100
# TODO: If given a directory, restore the entire contents from
1101
# the previous version.
1103
# TODO: Make a backup to a temporary file.
1105
# TODO: If the file previously didn't exist, delete it?
1106
for fn, file_id in nids:
1109
f = AtomicFile(fn, 'wb')
1111
f.write(old_tree.get_file(file_id).read())
1117
def pending_merges(self):
1118
"""Return a list of pending merges.
1120
These are revisions that have been merged into the working
1121
directory but not yet committed.
1123
cfn = self.controlfilename('pending-merges')
1124
if not os.path.exists(cfn):
1127
for l in self.controlfile('pending-merges', 'r').readlines():
1128
p.append(l.rstrip('\n'))
1132
def add_pending_merge(self, revision_id):
1133
from bzrlib.revision import validate_revision_id
1135
validate_revision_id(revision_id)
1137
p = self.pending_merges()
1138
if revision_id in p:
1140
p.append(revision_id)
1141
self.set_pending_merges(p)
1144
def set_pending_merges(self, rev_list):
1145
from bzrlib.atomicfile import AtomicFile
1148
f = AtomicFile(self.controlfilename('pending-merges'))
1159
def get_parent(self):
1160
"""Return the parent location of the branch.
1162
This is the default location for push/pull/missing. The usual
1163
pattern is that the user can override it by specifying a
1167
_locs = ['parent', 'pull', 'x-pull']
1170
return self.controlfile(l, 'r').read().strip('\n')
1172
if e.errno != errno.ENOENT:
1177
def set_parent(self, url):
1178
# TODO: Maybe delete old location files?
1179
from bzrlib.atomicfile import AtomicFile
1182
f = AtomicFile(self.controlfilename('parent'))
1191
def check_revno(self, revno):
1193
Check whether a revno corresponds to any revision.
1194
Zero (the NULL revision) is considered valid.
1197
self.check_real_revno(revno)
1199
def check_real_revno(self, revno):
1201
Check whether a revno corresponds to a real revision.
1202
Zero (the NULL revision) is considered invalid
1204
if revno < 1 or revno > self.revno():
1205
raise InvalidRevisionNumber(revno)
1211
class ScratchBranch(LocalBranch):
716
1212
"""Special test class: a branch that cleans up after itself.
718
1214
>>> b = ScratchBranch()
791
1321
name = name[idx+1 : ]
1323
# make it not a hidden file
793
1324
name = name.lstrip('.')
1326
# remove any wierd characters; we don't escape them but rather
1327
# just pull them out
1328
name = re.sub(r'[^\w.]', '', name)
795
1330
s = hexlify(rand_bytes(8))
796
return '-'.join((name, compact_date(time.time()), s))
1331
return '-'.join((name, compact_date(time()), s))
1335
"""Return a new tree-root file id."""
1336
return gen_file_id('TREE_ROOT')
1339
def copy_branch(branch_from, to_location, revno=None):
1340
"""Copy branch_from into the existing directory to_location.
1343
If not None, only revisions up to this point will be copied.
1344
The head of the new branch will be that revision.
1347
The name of a local directory that exists but is empty.
1349
from bzrlib.merge import merge
1351
assert isinstance(branch_from, Branch)
1352
assert isinstance(to_location, basestring)
1354
br_to = Branch.initialize(to_location)
1355
br_to.set_root_id(branch_from.get_root_id())
1357
revno = branch_from.revno()
1358
br_to.update_revisions(branch_from, stop_revision=revno)
1359
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1360
check_clean=False, ignore_zero=True)
1361
br_to.set_parent(branch_from.base)