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
36
from diff import diff_trees
22
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
29
from bzrlib.textui import show_status
30
from bzrlib.revision import Revision
31
from bzrlib.delta import compare_trees
32
from bzrlib.tree import EmptyTree, RevisionTree
38
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
39
## TODO: Maybe include checks for common corruption of newlines, etc?
42
# TODO: Some operations like log might retrieve the same revisions
43
# repeatedly to calculate deltas. We could perhaps have a weakref
44
# cache in memory to make this faster.
46
# TODO: please move the revision-string syntax stuff out of the branch
47
# object; it's clutter
43
50
def find_branch(f, **args):
44
51
if f and (f.startswith('http://') or f.startswith('https://')):
132
212
__repr__ = __str__
136
def lock(self, mode='w'):
137
"""Lock the on-disk branch, excluding other processes."""
143
om = os.O_WRONLY | os.O_CREAT
148
raise BzrError("invalid locking mode %r" % mode)
151
lockfile = os.open(self.controlfilename('branch-lock'), om)
153
if e.errno == errno.ENOENT:
154
# might not exist on branches from <0.0.4
155
self.controlfile('branch-lock', 'w').close()
156
lockfile = os.open(self.controlfilename('branch-lock'), om)
160
fcntl.lockf(lockfile, lm)
162
fcntl.lockf(lockfile, fcntl.LOCK_UN)
164
self._lockmode = None
166
self._lockmode = mode
168
warning("please write a locking method for platform %r" % sys.platform)
170
self._lockmode = None
172
self._lockmode = mode
175
def _need_readlock(self):
176
if self._lockmode not in ['r', 'w']:
177
raise BzrError('need read lock on branch, only have %r' % self._lockmode)
179
def _need_writelock(self):
180
if self._lockmode not in ['w']:
181
raise BzrError('need write lock on branch, only have %r' % self._lockmode)
216
if self._lock_mode or self._lock:
217
from warnings import warn
218
warn("branch %r was not explicitly unlocked" % self)
222
def lock_write(self):
224
if self._lock_mode != 'w':
225
from errors import LockError
226
raise LockError("can't upgrade to a write lock from %r" %
228
self._lock_count += 1
230
from bzrlib.lock import WriteLock
232
self._lock = WriteLock(self.controlfilename('branch-lock'))
233
self._lock_mode = 'w'
239
assert self._lock_mode in ('r', 'w'), \
240
"invalid lock mode %r" % self._lock_mode
241
self._lock_count += 1
243
from bzrlib.lock import ReadLock
245
self._lock = ReadLock(self.controlfilename('branch-lock'))
246
self._lock_mode = 'r'
250
if not self._lock_mode:
251
from errors import LockError
252
raise LockError('branch %r is not locked' % (self))
254
if self._lock_count > 1:
255
self._lock_count -= 1
259
self._lock_mode = self._lock_count = None
184
261
def abspath(self, name):
185
262
"""Return absolute filename for something in the branch"""
186
263
return os.path.join(self.base, name)
189
265
def relpath(self, path):
190
266
"""Return path relative to this branch of something inside it.
192
268
Raises an error if path is not in this branch."""
193
rp = os.path.realpath(path)
195
if not rp.startswith(self.base):
196
bailout("path %r is not within branch %r" % (rp, self.base))
197
rp = rp[len(self.base):]
198
rp = rp.lstrip(os.sep)
269
return _relpath(self.base, path)
202
271
def controlfilename(self, file_or_path):
203
272
"""Return location relative to branch."""
204
if isinstance(file_or_path, types.StringTypes):
273
if isinstance(file_or_path, basestring):
205
274
file_or_path = [file_or_path]
206
275
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
286
378
That is to say, the inventory describing changes underway, that
287
379
will be committed to the next revision.
289
self._need_writelock()
290
## TODO: factor out to atomicfile? is rename safe on windows?
291
## TODO: Maybe some kind of clean/dirty marker on inventory?
292
tmpfname = self.controlfilename('inventory.tmp')
293
tmpf = file(tmpfname, 'wb')
296
inv_fname = self.controlfilename('inventory')
297
if sys.platform == 'win32':
299
os.rename(tmpfname, inv_fname)
381
from bzrlib.atomicfile import AtomicFile
385
f = AtomicFile(self.controlfilename('inventory'), 'wb')
387
bzrlib.xml.serializer_v4.write_inventory(inv, f)
300
394
mutter('wrote working inventory')
303
397
inventory = property(read_working_inventory, _write_inventory, None,
304
398
"""Inventory for the working copy.""")
307
def add(self, files, verbose=False):
401
def add(self, files, ids=None):
308
402
"""Make files versioned.
310
Note that the command line normally calls smart_add instead.
404
Note that the command line normally calls smart_add instead,
405
which can automatically recurse.
312
407
This puts the files in the Added state, so that they will be
313
408
recorded by the next commit.
411
List of paths to add, relative to the base of the tree.
414
If set, use these instead of automatically generated ids.
415
Must be the same length as the list of files, but may
416
contain None for ids that are to be autogenerated.
315
418
TODO: Perhaps have an option to add the ids even if the files do
318
TODO: Perhaps return the ids of the files? But then again it
319
is easy to retrieve them if they're needed.
321
TODO: Option to specify file id.
323
TODO: Adding a directory should optionally recurse down and
324
add all non-ignored children. Perhaps do that in a
327
>>> b = ScratchBranch(files=['foo'])
328
>>> 'foo' in b.unknowns()
333
>>> 'foo' in b.unknowns()
335
>>> bool(b.inventory.path2id('foo'))
341
Traceback (most recent call last):
343
BzrError: ('foo is already versioned', [])
345
>>> b.add(['nothere'])
346
Traceback (most recent call last):
347
BzrError: ('cannot add: not a regular file or directory: nothere', [])
421
TODO: Perhaps yield the ids and paths as they're added.
349
self._need_writelock()
351
423
# TODO: Re-adding a file that is removed in the working copy
352
424
# should probably put it back with the previous ID.
353
if isinstance(files, types.StringTypes):
425
if isinstance(files, basestring):
426
assert(ids is None or isinstance(ids, basestring))
356
inv = self.read_working_inventory()
358
if is_control_file(f):
359
bailout("cannot add control file %s" % quotefn(f))
364
bailout("cannot add top-level %r" % f)
366
fullpath = os.path.normpath(self.abspath(f))
369
kind = file_kind(fullpath)
371
# maybe something better?
372
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
374
if kind != 'file' and kind != 'directory':
375
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
377
file_id = gen_file_id(f)
378
inv.add_path(f, kind=kind, file_id=file_id)
381
show_status('A', kind, quotefn(f))
383
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
385
self._write_inventory(inv)
432
ids = [None] * len(files)
434
assert(len(ids) == len(files))
438
inv = self.read_working_inventory()
439
for f,file_id in zip(files, ids):
440
if is_control_file(f):
441
raise BzrError("cannot add control file %s" % quotefn(f))
446
raise BzrError("cannot add top-level %r" % f)
448
fullpath = os.path.normpath(self.abspath(f))
451
kind = file_kind(fullpath)
453
# maybe something better?
454
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
456
if kind != 'file' and kind != 'directory':
457
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
460
file_id = gen_file_id(f)
461
inv.add_path(f, kind=kind, file_id=file_id)
463
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
465
self._write_inventory(inv)
388
470
def print_file(self, file, revno):
389
471
"""Print `file` to stdout."""
390
self._need_readlock()
391
tree = self.revision_tree(self.lookup_revision(revno))
392
# use inventory as it was in that revision
393
file_id = tree.inventory.path2id(file)
395
bailout("%r is not present in revision %d" % (file, revno))
396
tree.print_file(file_id)
474
tree = self.revision_tree(self.lookup_revision(revno))
475
# use inventory as it was in that revision
476
file_id = tree.inventory.path2id(file)
478
raise BzrError("%r is not present in revision %s" % (file, revno))
479
tree.print_file(file_id)
399
484
def remove(self, files, verbose=False):
400
485
"""Mark nominated files for removal from the inventory.
479
557
return self.working_tree().unknowns()
482
def commit(self, message, timestamp=None, timezone=None,
485
"""Commit working copy as a new revision.
487
The basic approach is to add all the file texts into the
488
store, then the inventory, then make a new revision pointing
489
to that inventory and store that.
491
This is not quite safe if the working copy changes during the
492
commit; for the moment that is simply not allowed. A better
493
approach is to make a temporary copy of the files before
494
computing their hashes, and then add those hashes in turn to
495
the inventory. This should mean at least that there are no
496
broken hash pointers. There is no way we can get a snapshot
497
of the whole directory at an instant. This would also have to
498
be robust against files disappearing, moving, etc. So the
499
whole thing is a bit hard.
501
timestamp -- if not None, seconds-since-epoch for a
502
postdated/predated commit.
504
self._need_writelock()
506
## TODO: Show branch names
508
# TODO: Don't commit if there are no changes, unless forced?
510
# First walk over the working inventory; and both update that
511
# and also build a new revision inventory. The revision
512
# inventory needs to hold the text-id, sha1 and size of the
513
# actual file versions committed in the revision. (These are
514
# not present in the working inventory.) We also need to
515
# detect missing/deleted files, and remove them from the
518
work_inv = self.read_working_inventory()
520
basis = self.basis_tree()
521
basis_inv = basis.inventory
523
for path, entry in work_inv.iter_entries():
524
## TODO: Cope with files that have gone missing.
526
## TODO: Check that the file kind has not changed from the previous
527
## revision of this file (if any).
531
p = self.abspath(path)
532
file_id = entry.file_id
533
mutter('commit prep file %s, id %r ' % (p, file_id))
535
if not os.path.exists(p):
536
mutter(" file is missing, removing from inventory")
538
show_status('D', entry.kind, quotefn(path))
539
missing_ids.append(file_id)
542
# TODO: Handle files that have been deleted
544
# TODO: Maybe a special case for empty files? Seems a
545
# waste to store them many times.
549
if basis_inv.has_id(file_id):
550
old_kind = basis_inv[file_id].kind
551
if old_kind != entry.kind:
552
bailout("entry %r changed kind from %r to %r"
553
% (file_id, old_kind, entry.kind))
555
if entry.kind == 'directory':
557
bailout("%s is entered as directory but not a directory" % quotefn(p))
558
elif entry.kind == 'file':
560
bailout("%s is entered as file but is not a file" % quotefn(p))
562
content = file(p, 'rb').read()
564
entry.text_sha1 = sha_string(content)
565
entry.text_size = len(content)
567
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
569
and (old_ie.text_size == entry.text_size)
570
and (old_ie.text_sha1 == entry.text_sha1)):
571
## assert content == basis.get_file(file_id).read()
572
entry.text_id = basis_inv[file_id].text_id
573
mutter(' unchanged from previous text_id {%s}' %
577
entry.text_id = gen_file_id(entry.name)
578
self.text_store.add(content, entry.text_id)
579
mutter(' stored with text_id {%s}' % entry.text_id)
583
elif (old_ie.name == entry.name
584
and old_ie.parent_id == entry.parent_id):
589
show_status(state, entry.kind, quotefn(path))
591
for file_id in missing_ids:
592
# have to do this later so we don't mess up the iterator.
593
# since parents may be removed before their children we
596
# FIXME: There's probably a better way to do this; perhaps
597
# the workingtree should know how to filter itself.
598
if work_inv.has_id(file_id):
599
del work_inv[file_id]
602
inv_id = rev_id = _gen_revision_id(time.time())
604
inv_tmp = tempfile.TemporaryFile()
605
inv.write_xml(inv_tmp)
607
self.inventory_store.add(inv_tmp, inv_id)
608
mutter('new inventory_id is {%s}' % inv_id)
610
self._write_inventory(work_inv)
612
if timestamp == None:
613
timestamp = time.time()
615
if committer == None:
616
committer = username()
619
timezone = local_time_offset()
621
mutter("building commit log message")
622
rev = Revision(timestamp=timestamp,
625
precursor = self.last_patch(),
630
rev_tmp = tempfile.TemporaryFile()
631
rev.write_xml(rev_tmp)
633
self.revision_store.add(rev_tmp, rev_id)
634
mutter("new revision_id is {%s}" % rev_id)
636
## XXX: Everything up to here can simply be orphaned if we abort
637
## the commit; it will leave junk files behind but that doesn't
640
## TODO: Read back the just-generated changeset, and make sure it
641
## applies and recreates the right state.
643
## TODO: Also calculate and store the inventory SHA1
644
mutter("committing patch r%d" % (self.revno() + 1))
647
self.append_revision(rev_id)
650
note("commited r%d" % self.revno())
653
def append_revision(self, revision_id):
654
mutter("add {%s} to revision-history" % revision_id)
560
def append_revision(self, *revision_ids):
561
from bzrlib.atomicfile import AtomicFile
563
for revision_id in revision_ids:
564
mutter("add {%s} to revision-history" % revision_id)
655
566
rev_history = self.revision_history()
657
tmprhname = self.controlfilename('revision-history.tmp')
658
rhname = self.controlfilename('revision-history')
660
f = file(tmprhname, 'wt')
661
rev_history.append(revision_id)
662
f.write('\n'.join(rev_history))
666
if sys.platform == 'win32':
668
os.rename(tmprhname, rhname)
567
rev_history.extend(revision_ids)
569
f = AtomicFile(self.controlfilename('revision-history'))
571
for rev_id in rev_history:
578
def get_revision_xml_file(self, revision_id):
579
"""Return XML file object for revision object."""
580
if not revision_id or not isinstance(revision_id, basestring):
581
raise InvalidRevisionId(revision_id)
586
return self.revision_store[revision_id]
588
raise bzrlib.errors.NoSuchRevision(self, revision_id)
594
get_revision_xml = get_revision_xml_file
672
597
def get_revision(self, revision_id):
673
598
"""Return the Revision object for a named revision"""
674
self._need_readlock()
675
r = Revision.read_xml(self.revision_store[revision_id])
599
xml_file = self.get_revision_xml_file(revision_id)
602
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
603
except SyntaxError, e:
604
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
676
608
assert r.revision_id == revision_id
612
def get_revision_delta(self, revno):
613
"""Return the delta for one revision.
615
The delta is relative to its mainline predecessor, or the
616
empty tree for revision 1.
618
assert isinstance(revno, int)
619
rh = self.revision_history()
620
if not (1 <= revno <= len(rh)):
621
raise InvalidRevisionNumber(revno)
623
# revno is 1-based; list is 0-based
625
new_tree = self.revision_tree(rh[revno-1])
627
old_tree = EmptyTree()
629
old_tree = self.revision_tree(rh[revno-2])
631
return compare_trees(old_tree, new_tree)
635
def get_revision_sha1(self, revision_id):
636
"""Hash the stored value of a revision, and return it."""
637
# In the future, revision entries will be signed. At that
638
# point, it is probably best *not* to include the signature
639
# in the revision hash. Because that lets you re-sign
640
# the revision, (add signatures/remove signatures) and still
641
# have all hash pointers stay consistent.
642
# But for now, just hash the contents.
643
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
680
646
def get_inventory(self, inventory_id):
681
647
"""Get Inventory object by hash.
683
649
TODO: Perhaps for this and similar methods, take a revision
684
650
parameter which can be either an integer revno or a
686
self._need_readlock()
687
i = Inventory.read_xml(self.inventory_store[inventory_id])
652
from bzrlib.inventory import Inventory
654
f = self.get_inventory_xml_file(inventory_id)
655
return bzrlib.xml.serializer_v4.read_inventory(f)
658
def get_inventory_xml(self, inventory_id):
659
"""Get inventory XML as a file object."""
660
return self.inventory_store[inventory_id]
662
get_inventory_xml_file = get_inventory_xml
665
def get_inventory_sha1(self, inventory_id):
666
"""Return the sha1 hash of the inventory entry
668
return sha_file(self.get_inventory_xml(inventory_id))
691
671
def get_revision_inventory(self, revision_id):
692
672
"""Return inventory of a past revision."""
693
self._need_readlock()
673
# bzr 0.0.6 imposes the constraint that the inventory_id
674
# must be the same as its revision, so this is trivial.
694
675
if revision_id == None:
676
from bzrlib.inventory import Inventory
677
return Inventory(self.get_root_id())
697
return self.get_inventory(self.get_revision(revision_id).inventory_id)
679
return self.get_inventory(revision_id)
700
682
def revision_history(self):
735
744
That is equivalent to the number of revisions committed to
738
>>> b = ScratchBranch()
741
>>> b.commit('no foo')
745
747
return len(self.revision_history())
748
750
def last_patch(self):
749
751
"""Return last patch hash, or None if no history.
751
>>> ScratchBranch().last_patch() == None
754
753
ph = self.revision_history()
761
def lookup_revision(self, revno):
762
"""Return revision hash for revision number."""
760
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
762
If self and other have not diverged, return a list of the revisions
763
present in other, but missing from self.
765
>>> from bzrlib.commit import commit
766
>>> bzrlib.trace.silent = True
767
>>> br1 = ScratchBranch()
768
>>> br2 = ScratchBranch()
769
>>> br1.missing_revisions(br2)
771
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
772
>>> br1.missing_revisions(br2)
774
>>> br2.missing_revisions(br1)
776
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
777
>>> br1.missing_revisions(br2)
779
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
780
>>> br1.missing_revisions(br2)
782
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
783
>>> br1.missing_revisions(br2)
784
Traceback (most recent call last):
785
DivergedBranches: These branches have diverged.
787
self_history = self.revision_history()
788
self_len = len(self_history)
789
other_history = other.revision_history()
790
other_len = len(other_history)
791
common_index = min(self_len, other_len) -1
792
if common_index >= 0 and \
793
self_history[common_index] != other_history[common_index]:
794
raise DivergedBranches(self, other)
796
if stop_revision is None:
797
stop_revision = other_len
798
elif stop_revision > other_len:
799
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
801
return other_history[self_len:stop_revision]
804
def update_revisions(self, other, stop_revision=None):
805
"""Pull in all new revisions from other branch.
807
from bzrlib.fetch import greedy_fetch
808
from bzrlib.revision import get_intervening_revisions
810
pb = bzrlib.ui.ui_factory.progress_bar()
811
pb.update('comparing histories')
814
revision_ids = self.missing_revisions(other, stop_revision)
815
except DivergedBranches, e:
817
if stop_revision is None:
818
end_revision = other.last_patch()
819
revision_ids = get_intervening_revisions(self.last_patch(),
821
assert self.last_patch() not in revision_ids
822
except bzrlib.errors.NotAncestor:
825
if len(revision_ids) > 0:
826
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
829
self.append_revision(*revision_ids)
830
## note("Added %d revisions." % count)
833
def install_revisions(self, other, revision_ids, pb):
834
if hasattr(other.revision_store, "prefetch"):
835
other.revision_store.prefetch(revision_ids)
836
if hasattr(other.inventory_store, "prefetch"):
837
inventory_ids = [other.get_revision(r).inventory_id
838
for r in revision_ids]
839
other.inventory_store.prefetch(inventory_ids)
842
pb = bzrlib.ui.ui_factory.progress_bar()
849
for i, rev_id in enumerate(revision_ids):
850
pb.update('fetching revision', i+1, len(revision_ids))
852
rev = other.get_revision(rev_id)
853
except bzrlib.errors.NoSuchRevision:
857
revisions.append(rev)
858
inv = other.get_inventory(str(rev.inventory_id))
859
for key, entry in inv.iter_entries():
860
if entry.text_id is None:
862
if entry.text_id not in self.text_store:
863
needed_texts.add(entry.text_id)
867
count, cp_fail = self.text_store.copy_multi(other.text_store,
869
#print "Added %d texts." % count
870
inventory_ids = [ f.inventory_id for f in revisions ]
871
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
873
#print "Added %d inventories." % count
874
revision_ids = [ f.revision_id for f in revisions]
876
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
879
assert len(cp_fail) == 0
880
return count, failures
883
def commit(self, *args, **kw):
884
from bzrlib.commit import commit
885
commit(self, *args, **kw)
888
def lookup_revision(self, revision):
889
"""Return the revision identifier for a given revision information."""
890
revno, info = self._get_revision_info(revision)
894
def revision_id_to_revno(self, revision_id):
895
"""Given a revision id, return its revno"""
896
history = self.revision_history()
898
return history.index(revision_id) + 1
900
raise bzrlib.errors.NoSuchRevision(self, revision_id)
903
def get_revision_info(self, revision):
904
"""Return (revno, revision id) for revision identifier.
906
revision can be an integer, in which case it is assumed to be revno (though
907
this will translate negative values into positive ones)
908
revision can also be a string, in which case it is parsed for something like
909
'date:' or 'revid:' etc.
911
revno, rev_id = self._get_revision_info(revision)
913
raise bzrlib.errors.NoSuchRevision(self, revision)
916
def get_rev_id(self, revno, history=None):
917
"""Find the revision id of the specified revno."""
767
# list is 0-based; revisions are 1-based
768
return self.revision_history()[revno-1]
770
raise BzrError("no such revision %s" % revno)
921
history = self.revision_history()
922
elif revno <= 0 or revno > len(history):
923
raise bzrlib.errors.NoSuchRevision(self, revno)
924
return history[revno - 1]
926
def _get_revision_info(self, revision):
927
"""Return (revno, revision id) for revision specifier.
929
revision can be an integer, in which case it is assumed to be revno
930
(though this will translate negative values into positive ones)
931
revision can also be a string, in which case it is parsed for something
932
like 'date:' or 'revid:' etc.
934
A revid is always returned. If it is None, the specifier referred to
935
the null revision. If the revid does not occur in the revision
936
history, revno will be None.
942
try:# Convert to int if possible
943
revision = int(revision)
946
revs = self.revision_history()
947
if isinstance(revision, int):
949
revno = len(revs) + revision + 1
952
rev_id = self.get_rev_id(revno, revs)
953
elif isinstance(revision, basestring):
954
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
955
if revision.startswith(prefix):
956
result = func(self, revs, revision)
958
revno, rev_id = result
961
rev_id = self.get_rev_id(revno, revs)
964
raise BzrError('No namespace registered for string: %r' %
967
raise TypeError('Unhandled revision type %s' % revision)
971
raise bzrlib.errors.NoSuchRevision(self, revision)
974
def _namespace_revno(self, revs, revision):
975
"""Lookup a revision by revision number"""
976
assert revision.startswith('revno:')
978
return (int(revision[6:]),)
981
REVISION_NAMESPACES['revno:'] = _namespace_revno
983
def _namespace_revid(self, revs, revision):
984
assert revision.startswith('revid:')
985
rev_id = revision[len('revid:'):]
987
return revs.index(rev_id) + 1, rev_id
990
REVISION_NAMESPACES['revid:'] = _namespace_revid
992
def _namespace_last(self, revs, revision):
993
assert revision.startswith('last:')
995
offset = int(revision[5:])
1000
raise BzrError('You must supply a positive value for --revision last:XXX')
1001
return (len(revs) - offset + 1,)
1002
REVISION_NAMESPACES['last:'] = _namespace_last
1004
def _namespace_tag(self, revs, revision):
1005
assert revision.startswith('tag:')
1006
raise BzrError('tag: namespace registered, but not implemented.')
1007
REVISION_NAMESPACES['tag:'] = _namespace_tag
1009
def _namespace_date(self, revs, revision):
1010
assert revision.startswith('date:')
1012
# Spec for date revisions:
1014
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1015
# it can also start with a '+/-/='. '+' says match the first
1016
# entry after the given date. '-' is match the first entry before the date
1017
# '=' is match the first entry after, but still on the given date.
1019
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1020
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1021
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1022
# May 13th, 2005 at 0:00
1024
# So the proper way of saying 'give me all entries for today' is:
1025
# -r {date:+today}:{date:-tomorrow}
1026
# The default is '=' when not supplied
1029
if val[:1] in ('+', '-', '='):
1030
match_style = val[:1]
1033
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1034
if val.lower() == 'yesterday':
1035
dt = today - datetime.timedelta(days=1)
1036
elif val.lower() == 'today':
1038
elif val.lower() == 'tomorrow':
1039
dt = today + datetime.timedelta(days=1)
1042
# This should be done outside the function to avoid recompiling it.
1043
_date_re = re.compile(
1044
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1046
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1048
m = _date_re.match(val)
1049
if not m or (not m.group('date') and not m.group('time')):
1050
raise BzrError('Invalid revision date %r' % revision)
1053
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1055
year, month, day = today.year, today.month, today.day
1057
hour = int(m.group('hour'))
1058
minute = int(m.group('minute'))
1059
if m.group('second'):
1060
second = int(m.group('second'))
1064
hour, minute, second = 0,0,0
1066
dt = datetime.datetime(year=year, month=month, day=day,
1067
hour=hour, minute=minute, second=second)
1071
if match_style == '-':
1073
elif match_style == '=':
1074
last = dt + datetime.timedelta(days=1)
1077
for i in range(len(revs)-1, -1, -1):
1078
r = self.get_revision(revs[i])
1079
# TODO: Handle timezone.
1080
dt = datetime.datetime.fromtimestamp(r.timestamp)
1081
if first >= dt and (last is None or dt >= last):
1084
for i in range(len(revs)):
1085
r = self.get_revision(revs[i])
1086
# TODO: Handle timezone.
1087
dt = datetime.datetime.fromtimestamp(r.timestamp)
1088
if first <= dt and (last is None or dt <= last):
1090
REVISION_NAMESPACES['date:'] = _namespace_date
773
1092
def revision_tree(self, revision_id):
774
1093
"""Return Tree for a revision on this branch.
776
1095
`revision_id` may be None for the null revision, in which case
777
1096
an `EmptyTree` is returned."""
778
self._need_readlock()
1097
# TODO: refactor this to use an existing revision object
1098
# so we don't need to read it in twice.
779
1099
if revision_id == None:
780
1100
return EmptyTree()
818
1128
This can change the directory or the filename or both.
820
self._need_writelock()
821
tree = self.working_tree()
823
if not tree.has_filename(from_rel):
824
bailout("can't rename: old working file %r does not exist" % from_rel)
825
if tree.has_filename(to_rel):
826
bailout("can't rename: new working file %r already exists" % to_rel)
828
file_id = inv.path2id(from_rel)
830
bailout("can't rename: old name %r is not versioned" % from_rel)
832
if inv.path2id(to_rel):
833
bailout("can't rename: new name %r is already versioned" % to_rel)
835
to_dir, to_tail = os.path.split(to_rel)
836
to_dir_id = inv.path2id(to_dir)
837
if to_dir_id == None and to_dir != '':
838
bailout("can't determine destination directory id for %r" % to_dir)
840
mutter("rename_one:")
841
mutter(" file_id {%s}" % file_id)
842
mutter(" from_rel %r" % from_rel)
843
mutter(" to_rel %r" % to_rel)
844
mutter(" to_dir %r" % to_dir)
845
mutter(" to_dir_id {%s}" % to_dir_id)
847
inv.rename(file_id, to_dir_id, to_tail)
849
print "%s => %s" % (from_rel, to_rel)
851
from_abs = self.abspath(from_rel)
852
to_abs = self.abspath(to_rel)
854
os.rename(from_abs, to_abs)
856
bailout("failed to rename %r to %r: %s"
857
% (from_abs, to_abs, e[1]),
858
["rename rolled back"])
860
self._write_inventory(inv)
1132
tree = self.working_tree()
1133
inv = tree.inventory
1134
if not tree.has_filename(from_rel):
1135
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1136
if tree.has_filename(to_rel):
1137
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1139
file_id = inv.path2id(from_rel)
1141
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1143
if inv.path2id(to_rel):
1144
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1146
to_dir, to_tail = os.path.split(to_rel)
1147
to_dir_id = inv.path2id(to_dir)
1148
if to_dir_id == None and to_dir != '':
1149
raise BzrError("can't determine destination directory id for %r" % to_dir)
1151
mutter("rename_one:")
1152
mutter(" file_id {%s}" % file_id)
1153
mutter(" from_rel %r" % from_rel)
1154
mutter(" to_rel %r" % to_rel)
1155
mutter(" to_dir %r" % to_dir)
1156
mutter(" to_dir_id {%s}" % to_dir_id)
1158
inv.rename(file_id, to_dir_id, to_tail)
1160
from_abs = self.abspath(from_rel)
1161
to_abs = self.abspath(to_rel)
1163
os.rename(from_abs, to_abs)
1165
raise BzrError("failed to rename %r to %r: %s"
1166
% (from_abs, to_abs, e[1]),
1167
["rename rolled back"])
1169
self._write_inventory(inv)
864
1174
def move(self, from_paths, to_name):
872
1182
Note that to_name is only the last component of the new name;
873
1183
this doesn't change the directory.
875
self._need_writelock()
876
## TODO: Option to move IDs only
877
assert not isinstance(from_paths, basestring)
878
tree = self.working_tree()
880
to_abs = self.abspath(to_name)
881
if not isdir(to_abs):
882
bailout("destination %r is not a directory" % to_abs)
883
if not tree.has_filename(to_name):
884
bailout("destination %r not in working directory" % to_abs)
885
to_dir_id = inv.path2id(to_name)
886
if to_dir_id == None and to_name != '':
887
bailout("destination %r is not a versioned directory" % to_name)
888
to_dir_ie = inv[to_dir_id]
889
if to_dir_ie.kind not in ('directory', 'root_directory'):
890
bailout("destination %r is not a directory" % to_abs)
892
to_idpath = Set(inv.get_idpath(to_dir_id))
895
if not tree.has_filename(f):
896
bailout("%r does not exist in working tree" % f)
897
f_id = inv.path2id(f)
899
bailout("%r is not versioned" % f)
900
name_tail = splitpath(f)[-1]
901
dest_path = appendpath(to_name, name_tail)
902
if tree.has_filename(dest_path):
903
bailout("destination %r already exists" % dest_path)
904
if f_id in to_idpath:
905
bailout("can't move %r to a subdirectory of itself" % f)
907
# OK, so there's a race here, it's possible that someone will
908
# create a file in this interval and then the rename might be
909
# left half-done. But we should have caught most problems.
912
name_tail = splitpath(f)[-1]
913
dest_path = appendpath(to_name, name_tail)
914
print "%s => %s" % (f, dest_path)
915
inv.rename(inv.path2id(f), to_dir_id, name_tail)
917
os.rename(self.abspath(f), self.abspath(dest_path))
919
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
920
["rename rolled back"])
922
self._write_inventory(inv)
926
def show_status(self, show_all=False, file_list=None):
927
"""Display single-line status for non-ignored working files.
929
The list is show sorted in order by file name.
931
>>> b = ScratchBranch(files=['foo', 'foo~'])
937
>>> b.commit("add foo")
939
>>> os.unlink(b.abspath('foo'))
943
self._need_readlock()
945
# We have to build everything into a list first so that it can
946
# sorted by name, incorporating all the different sources.
948
# FIXME: Rather than getting things in random order and then sorting,
949
# just step through in order.
951
# Interesting case: the old ID for a file has been removed,
952
# but a new file has been created under that name.
954
old = self.basis_tree()
955
new = self.working_tree()
957
items = diff_trees(old, new)
958
# We want to filter out only if any file was provided in the file_list.
959
if isinstance(file_list, list) and len(file_list):
960
items = [item for item in items if item[3] in file_list]
962
for fs, fid, oldname, newname, kind in items:
964
show_status(fs, kind,
965
oldname + ' => ' + newname)
966
elif fs == 'A' or fs == 'M':
967
show_status(fs, kind, newname)
969
show_status(fs, kind, oldname)
972
show_status(fs, kind, newname)
975
show_status(fs, kind, newname)
977
show_status(fs, kind, newname)
979
bailout("weird file state %r" % ((fs, fid),))
1185
This returns a list of (from_path, to_path) pairs for each
1186
entry that is moved.
1191
## TODO: Option to move IDs only
1192
assert not isinstance(from_paths, basestring)
1193
tree = self.working_tree()
1194
inv = tree.inventory
1195
to_abs = self.abspath(to_name)
1196
if not isdir(to_abs):
1197
raise BzrError("destination %r is not a directory" % to_abs)
1198
if not tree.has_filename(to_name):
1199
raise BzrError("destination %r not in working directory" % to_abs)
1200
to_dir_id = inv.path2id(to_name)
1201
if to_dir_id == None and to_name != '':
1202
raise BzrError("destination %r is not a versioned directory" % to_name)
1203
to_dir_ie = inv[to_dir_id]
1204
if to_dir_ie.kind not in ('directory', 'root_directory'):
1205
raise BzrError("destination %r is not a directory" % to_abs)
1207
to_idpath = inv.get_idpath(to_dir_id)
1209
for f in from_paths:
1210
if not tree.has_filename(f):
1211
raise BzrError("%r does not exist in working tree" % f)
1212
f_id = inv.path2id(f)
1214
raise BzrError("%r is not versioned" % f)
1215
name_tail = splitpath(f)[-1]
1216
dest_path = appendpath(to_name, name_tail)
1217
if tree.has_filename(dest_path):
1218
raise BzrError("destination %r already exists" % dest_path)
1219
if f_id in to_idpath:
1220
raise BzrError("can't move %r to a subdirectory of itself" % f)
1222
# OK, so there's a race here, it's possible that someone will
1223
# create a file in this interval and then the rename might be
1224
# left half-done. But we should have caught most problems.
1226
for f in from_paths:
1227
name_tail = splitpath(f)[-1]
1228
dest_path = appendpath(to_name, name_tail)
1229
result.append((f, dest_path))
1230
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1232
os.rename(self.abspath(f), self.abspath(dest_path))
1234
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1235
["rename rolled back"])
1237
self._write_inventory(inv)
1244
def revert(self, filenames, old_tree=None, backups=True):
1245
"""Restore selected files to the versions from a previous tree.
1248
If true (default) backups are made of files before
1251
from bzrlib.errors import NotVersionedError, BzrError
1252
from bzrlib.atomicfile import AtomicFile
1253
from bzrlib.osutils import backup_file
1255
inv = self.read_working_inventory()
1256
if old_tree is None:
1257
old_tree = self.basis_tree()
1258
old_inv = old_tree.inventory
1261
for fn in filenames:
1262
file_id = inv.path2id(fn)
1264
raise NotVersionedError("not a versioned file", fn)
1265
if not old_inv.has_id(file_id):
1266
raise BzrError("file not present in old tree", fn, file_id)
1267
nids.append((fn, file_id))
1269
# TODO: Rename back if it was previously at a different location
1271
# TODO: If given a directory, restore the entire contents from
1272
# the previous version.
1274
# TODO: Make a backup to a temporary file.
1276
# TODO: If the file previously didn't exist, delete it?
1277
for fn, file_id in nids:
1280
f = AtomicFile(fn, 'wb')
1282
f.write(old_tree.get_file(file_id).read())
1288
def pending_merges(self):
1289
"""Return a list of pending merges.
1291
These are revisions that have been merged into the working
1292
directory but not yet committed.
1294
cfn = self.controlfilename('pending-merges')
1295
if not os.path.exists(cfn):
1298
for l in self.controlfile('pending-merges', 'r').readlines():
1299
p.append(l.rstrip('\n'))
1303
def add_pending_merge(self, revision_id):
1304
from bzrlib.revision import validate_revision_id
1306
validate_revision_id(revision_id)
1308
p = self.pending_merges()
1309
if revision_id in p:
1311
p.append(revision_id)
1312
self.set_pending_merges(p)
1315
def set_pending_merges(self, rev_list):
1316
from bzrlib.atomicfile import AtomicFile
1319
f = AtomicFile(self.controlfilename('pending-merges'))
1330
def get_parent(self):
1331
"""Return the parent location of the branch.
1333
This is the default location for push/pull/missing. The usual
1334
pattern is that the user can override it by specifying a
1338
_locs = ['parent', 'pull', 'x-pull']
1341
return self.controlfile(l, 'r').read().strip('\n')
1343
if e.errno != errno.ENOENT:
1348
def set_parent(self, url):
1349
# TODO: Maybe delete old location files?
1350
from bzrlib.atomicfile import AtomicFile
1353
f = AtomicFile(self.controlfilename('parent'))
1362
def check_revno(self, revno):
1364
Check whether a revno corresponds to any revision.
1365
Zero (the NULL revision) is considered valid.
1368
self.check_real_revno(revno)
1370
def check_real_revno(self, revno):
1372
Check whether a revno corresponds to a real revision.
1373
Zero (the NULL revision) is considered invalid
1375
if revno < 1 or revno > self.revno():
1376
raise InvalidRevisionNumber(revno)
983
1381
class ScratchBranch(Branch):