~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil, Patch Queue Manager, Jelmer Vernooij
  • Date: 2017-01-17 16:20:41 UTC
  • mfrom: (6619.1.2 trunk)
  • Revision ID: tarmac-20170117162041-oo62uk1qsmgc9j31
Merge 2.7 into trunk including fixes for bugs #1622039, #1644003, #1579093 and #1645017. [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
from __future__ import absolute_import
17
18
 
18
 
import re
19
19
 
20
20
from bzrlib.lazy_import import lazy_import
21
21
lazy_import(globals(), """
22
22
import bisect
23
23
import datetime
 
24
 
 
25
from bzrlib import (
 
26
    branch as _mod_branch,
 
27
    osutils,
 
28
    revision,
 
29
    symbol_versioning,
 
30
    workingtree,
 
31
    )
 
32
from bzrlib.i18n import gettext
24
33
""")
25
34
 
26
35
from bzrlib import (
27
36
    errors,
28
 
    osutils,
 
37
    lazy_regex,
29
38
    registry,
30
 
    revision,
31
 
    symbol_versioning,
32
39
    trace,
33
40
    )
34
41
 
35
42
 
36
 
_marker = []
37
 
 
38
 
 
39
43
class RevisionInfo(object):
40
44
    """The results of applying a revision specification to a branch."""
41
45
 
53
57
    or treat the result as a tuple.
54
58
    """
55
59
 
56
 
    def __init__(self, branch, revno, rev_id=_marker):
 
60
    def __init__(self, branch, revno=None, rev_id=None):
57
61
        self.branch = branch
58
 
        self.revno = revno
59
 
        if rev_id is _marker:
 
62
        self._has_revno = (revno is not None)
 
63
        self._revno = revno
 
64
        self.rev_id = rev_id
 
65
        if self.rev_id is None and self._revno is not None:
60
66
            # allow caller to be lazy
61
 
            if self.revno is None:
62
 
                self.rev_id = None
63
 
            else:
64
 
                self.rev_id = branch.get_rev_id(self.revno)
65
 
        else:
66
 
            self.rev_id = rev_id
 
67
            self.rev_id = branch.get_rev_id(self._revno)
 
68
 
 
69
    @property
 
70
    def revno(self):
 
71
        if not self._has_revno and self.rev_id is not None:
 
72
            try:
 
73
                self._revno = self.branch.revision_id_to_revno(self.rev_id)
 
74
            except errors.NoSuchRevision:
 
75
                self._revno = None
 
76
            self._has_revno = True
 
77
        return self._revno
67
78
 
68
79
    def __nonzero__(self):
69
 
        # first the easy ones...
70
80
        if self.rev_id is None:
71
81
            return False
72
 
        if self.revno is not None:
73
 
            return True
74
82
        # TODO: otherwise, it should depend on how I was built -
75
83
        # if it's in_history(branch), then check revision_history(),
76
84
        # if it's in_store(branch), do the check below
99
107
            self.revno, self.rev_id, self.branch)
100
108
 
101
109
    @staticmethod
102
 
    def from_revision_id(branch, revision_id, revs):
 
110
    def from_revision_id(branch, revision_id, revs=symbol_versioning.DEPRECATED_PARAMETER):
103
111
        """Construct a RevisionInfo given just the id.
104
112
 
105
113
        Use this if you don't know or care what the revno is.
106
114
        """
107
 
        if revision_id == revision.NULL_REVISION:
108
 
            return RevisionInfo(branch, 0, revision_id)
109
 
        try:
110
 
            revno = revs.index(revision_id) + 1
111
 
        except ValueError:
112
 
            revno = None
113
 
        return RevisionInfo(branch, revno, revision_id)
114
 
 
115
 
 
116
 
_revno_regex = None
 
115
        if symbol_versioning.deprecated_passed(revs):
 
116
            symbol_versioning.warn(
 
117
                'RevisionInfo.from_revision_id(revs) was deprecated in 2.5.',
 
118
                DeprecationWarning,
 
119
                stacklevel=2)
 
120
        return RevisionInfo(branch, revno=None, rev_id=revision_id)
117
121
 
118
122
 
119
123
class RevisionSpec(object):
136
140
    """
137
141
 
138
142
    prefix = None
139
 
    wants_revision_history = True
 
143
    # wants_revision_history has been deprecated in 2.5.
 
144
    wants_revision_history = False
140
145
    dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
141
146
    """Exceptions that RevisionSpec_dwim._match_on will catch.
142
147
 
166
171
                         spectype.__name__, spec)
167
172
            return spectype(spec, _internal=True)
168
173
        else:
169
 
            for spectype in SPEC_TYPES:
170
 
                if spec.startswith(spectype.prefix):
171
 
                    trace.mutter('Returning RevisionSpec %s for %s',
172
 
                                 spectype.__name__, spec)
173
 
                    return spectype(spec, _internal=True)
174
174
            # Otherwise treat it as a DWIM, build the RevisionSpec object and
175
175
            # wait for _match_on to be called.
176
176
            return RevisionSpec_dwim(spec, _internal=True)
212
212
    def in_history(self, branch):
213
213
        if branch:
214
214
            if self.wants_revision_history:
215
 
                revs = branch.revision_history()
 
215
                symbol_versioning.warn(
 
216
                    "RevisionSpec.wants_revision_history was "
 
217
                    "deprecated in 2.5 (%s)." % self.__class__.__name__,
 
218
                    DeprecationWarning)
 
219
                branch.lock_read()
 
220
                try:
 
221
                    graph = branch.repository.get_graph()
 
222
                    revs = list(graph.iter_lefthand_ancestry(
 
223
                        branch.last_revision(), [revision.NULL_REVISION]))
 
224
                finally:
 
225
                    branch.unlock()
 
226
                revs.reverse()
216
227
            else:
217
228
                revs = None
218
229
        else:
298
309
    """
299
310
 
300
311
    help_txt = None
301
 
    # We don't need to build the revision history ourself, that's delegated to
302
 
    # each revspec we try.
303
 
    wants_revision_history = False
 
312
 
 
313
    _revno_regex = lazy_regex.lazy_compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
 
314
 
 
315
    # The revspecs to try
 
316
    _possible_revspecs = []
304
317
 
305
318
    def _try_spectype(self, rstype, branch):
306
319
        rs = rstype(self.spec, _internal=True)
312
325
        """Run the lookup and see what we can get."""
313
326
 
314
327
        # First, see if it's a revno
315
 
        global _revno_regex
316
 
        if _revno_regex is None:
317
 
            _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
318
 
        if _revno_regex.match(self.spec) is not None:
 
328
        if self._revno_regex.match(self.spec) is not None:
319
329
            try:
320
330
                return self._try_spectype(RevisionSpec_revno, branch)
321
331
            except RevisionSpec_revno.dwim_catchable_exceptions:
322
332
                pass
323
333
 
324
334
        # Next see what has been registered
 
335
        for objgetter in self._possible_revspecs:
 
336
            rs_class = objgetter.get_obj()
 
337
            try:
 
338
                return self._try_spectype(rs_class, branch)
 
339
            except rs_class.dwim_catchable_exceptions:
 
340
                pass
 
341
 
 
342
        # Try the old (deprecated) dwim list:
325
343
        for rs_class in dwim_revspecs:
326
344
            try:
327
345
                return self._try_spectype(rs_class, branch)
333
351
        # really relevant.
334
352
        raise errors.InvalidRevisionSpec(self.spec, branch)
335
353
 
 
354
    @classmethod
 
355
    def append_possible_revspec(cls, revspec):
 
356
        """Append a possible DWIM revspec.
 
357
 
 
358
        :param revspec: Revision spec to try.
 
359
        """
 
360
        cls._possible_revspecs.append(registry._ObjectGetter(revspec))
 
361
 
 
362
    @classmethod
 
363
    def append_possible_lazy_revspec(cls, module_name, member_name):
 
364
        """Append a possible lazily loaded DWIM revspec.
 
365
 
 
366
        :param module_name: Name of the module with the revspec
 
367
        :param member_name: Name of the revspec within the module
 
368
        """
 
369
        cls._possible_revspecs.append(
 
370
            registry._LazyObjectGetter(module_name, member_name))
 
371
 
336
372
 
337
373
class RevisionSpec_revno(RevisionSpec):
338
374
    """Selects a revision using a number."""
356
392
                                   your history is very long.
357
393
    """
358
394
    prefix = 'revno:'
359
 
    wants_revision_history = False
360
395
 
361
396
    def _match_on(self, branch, revs):
362
397
        """Lookup a revision by revision number"""
363
 
        branch, revno, revision_id = self._lookup(branch, revs)
 
398
        branch, revno, revision_id = self._lookup(branch)
364
399
        return RevisionInfo(branch, revno, revision_id)
365
400
 
366
 
    def _lookup(self, branch, revs_or_none):
 
401
    def _lookup(self, branch):
367
402
        loc = self.spec.find(':')
368
403
        if loc == -1:
369
404
            revno_spec = self.spec
393
428
                dotted = True
394
429
 
395
430
        if branch_spec:
396
 
            # the user has override the branch to look in.
397
 
            # we need to refresh the revision_history map and
398
 
            # the branch object.
399
 
            from bzrlib.branch import Branch
400
 
            branch = Branch.open(branch_spec)
401
 
            revs_or_none = None
 
431
            # the user has overriden the branch to look in.
 
432
            branch = _mod_branch.Branch.open(branch_spec)
402
433
 
403
434
        if dotted:
404
435
            try:
408
439
                raise errors.InvalidRevisionSpec(self.user_spec, branch)
409
440
            else:
410
441
                # there is no traditional 'revno' for dotted-decimal revnos.
411
 
                # so for  API compatability we return None.
 
442
                # so for API compatibility we return None.
412
443
                return branch, None, revision_id
413
444
        else:
414
445
            last_revno, last_revision_id = branch.last_revision_info()
420
451
                else:
421
452
                    revno = last_revno + revno + 1
422
453
            try:
423
 
                revision_id = branch.get_rev_id(revno, revs_or_none)
 
454
                revision_id = branch.get_rev_id(revno)
424
455
            except errors.NoSuchRevision:
425
456
                raise errors.InvalidRevisionSpec(self.user_spec, branch)
426
457
        return branch, revno, revision_id
427
458
 
428
459
    def _as_revision_id(self, context_branch):
429
460
        # We would have the revno here, but we don't really care
430
 
        branch, revno, revision_id = self._lookup(context_branch, None)
 
461
        branch, revno, revision_id = self._lookup(context_branch)
431
462
        return revision_id
432
463
 
433
464
    def needs_branch(self):
443
474
RevisionSpec_int = RevisionSpec_revno
444
475
 
445
476
 
446
 
 
447
 
class RevisionSpec_revid(RevisionSpec):
 
477
class RevisionIDSpec(RevisionSpec):
 
478
 
 
479
    def _match_on(self, branch, revs):
 
480
        revision_id = self.as_revision_id(branch)
 
481
        return RevisionInfo.from_revision_id(branch, revision_id)
 
482
 
 
483
 
 
484
class RevisionSpec_revid(RevisionIDSpec):
448
485
    """Selects a revision using the revision id."""
449
486
 
450
487
    help_txt = """Selects a revision using the revision id.
459
496
 
460
497
    prefix = 'revid:'
461
498
 
462
 
    def _match_on(self, branch, revs):
 
499
    def _as_revision_id(self, context_branch):
463
500
        # self.spec comes straight from parsing the command line arguments,
464
501
        # so we expect it to be a Unicode string. Switch it to the internal
465
502
        # representation.
466
 
        revision_id = osutils.safe_revision_id(self.spec, warn=False)
467
 
        return RevisionInfo.from_revision_id(branch, revision_id, revs)
468
 
 
469
 
    def _as_revision_id(self, context_branch):
470
503
        return osutils.safe_revision_id(self.spec, warn=False)
471
504
 
472
505
 
487
520
    prefix = 'last:'
488
521
 
489
522
    def _match_on(self, branch, revs):
490
 
        revno, revision_id = self._revno_and_revision_id(branch, revs)
 
523
        revno, revision_id = self._revno_and_revision_id(branch)
491
524
        return RevisionInfo(branch, revno, revision_id)
492
525
 
493
 
    def _revno_and_revision_id(self, context_branch, revs_or_none):
 
526
    def _revno_and_revision_id(self, context_branch):
494
527
        last_revno, last_revision_id = context_branch.last_revision_info()
495
528
 
496
529
        if self.spec == '':
509
542
 
510
543
        revno = last_revno - offset + 1
511
544
        try:
512
 
            revision_id = context_branch.get_rev_id(revno, revs_or_none)
 
545
            revision_id = context_branch.get_rev_id(revno)
513
546
        except errors.NoSuchRevision:
514
547
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
515
548
        return revno, revision_id
517
550
    def _as_revision_id(self, context_branch):
518
551
        # We compute the revno as part of the process, but we don't really care
519
552
        # about it.
520
 
        revno, revision_id = self._revno_and_revision_id(context_branch, None)
 
553
        revno, revision_id = self._revno_and_revision_id(context_branch)
521
554
        return revision_id
522
555
 
523
556
 
555
588
            # We need to use the repository history here
556
589
            rev = branch.repository.get_revision(r.rev_id)
557
590
            if not rev.parent_ids:
558
 
                revno = 0
559
591
                revision_id = revision.NULL_REVISION
560
592
            else:
561
593
                revision_id = rev.parent_ids[0]
562
 
                try:
563
 
                    revno = revs.index(revision_id) + 1
564
 
                except ValueError:
565
 
                    revno = None
 
594
            revno = None
566
595
        else:
567
596
            revno = r.revno - 1
568
597
            try:
573
602
        return RevisionInfo(branch, revno, revision_id)
574
603
 
575
604
    def _as_revision_id(self, context_branch):
576
 
        base_revspec = RevisionSpec.from_string(self.spec)
577
 
        base_revision_id = base_revspec.as_revision_id(context_branch)
 
605
        base_revision_id = RevisionSpec.from_string(self.spec)._as_revision_id(context_branch)
578
606
        if base_revision_id == revision.NULL_REVISION:
579
607
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
580
608
                                         'cannot go before the null: revision')
610
638
    def _match_on(self, branch, revs):
611
639
        # Can raise tags not supported, NoSuchTag, etc
612
640
        return RevisionInfo.from_revision_id(branch,
613
 
            branch.tags.lookup_tag(self.spec),
614
 
            revs)
 
641
            branch.tags.lookup_tag(self.spec))
615
642
 
616
643
    def _as_revision_id(self, context_branch):
617
644
        return context_branch.tags.lookup_tag(self.spec)
621
648
class _RevListToTimestamps(object):
622
649
    """This takes a list of revisions, and allows you to bisect by date"""
623
650
 
624
 
    __slots__ = ['revs', 'branch']
 
651
    __slots__ = ['branch']
625
652
 
626
 
    def __init__(self, revs, branch):
627
 
        self.revs = revs
 
653
    def __init__(self, branch):
628
654
        self.branch = branch
629
655
 
630
656
    def __getitem__(self, index):
631
657
        """Get the date of the index'd item"""
632
 
        r = self.branch.repository.get_revision(self.revs[index])
 
658
        r = self.branch.repository.get_revision(self.branch.get_rev_id(index))
633
659
        # TODO: Handle timezone.
634
660
        return datetime.datetime.fromtimestamp(r.timestamp)
635
661
 
636
662
    def __len__(self):
637
 
        return len(self.revs)
 
663
        return self.branch.revno()
638
664
 
639
665
 
640
666
class RevisionSpec_date(RevisionSpec):
658
684
                                   August 14th, 2006 at 5:10pm.
659
685
    """
660
686
    prefix = 'date:'
661
 
    _date_re = re.compile(
 
687
    _date_regex = lazy_regex.lazy_compile(
662
688
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
663
689
            r'(,|T)?\s*'
664
690
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
682
708
        elif self.spec.lower() == 'tomorrow':
683
709
            dt = today + datetime.timedelta(days=1)
684
710
        else:
685
 
            m = self._date_re.match(self.spec)
 
711
            m = self._date_regex.match(self.spec)
686
712
            if not m or (not m.group('date') and not m.group('time')):
687
713
                raise errors.InvalidRevisionSpec(self.user_spec,
688
714
                                                 branch, 'invalid date')
714
740
                    hour=hour, minute=minute, second=second)
715
741
        branch.lock_read()
716
742
        try:
717
 
            rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
 
743
            rev = bisect.bisect(_RevListToTimestamps(branch), dt, 1)
718
744
        finally:
719
745
            branch.unlock()
720
 
        if rev == len(revs):
 
746
        if rev == branch.revno():
721
747
            raise errors.InvalidRevisionSpec(self.user_spec, branch)
722
 
        else:
723
 
            return RevisionInfo(branch, rev + 1)
 
748
        return RevisionInfo(branch, rev)
724
749
 
725
750
 
726
751
 
757
782
    def _find_revision_info(branch, other_location):
758
783
        revision_id = RevisionSpec_ancestor._find_revision_id(branch,
759
784
                                                              other_location)
760
 
        try:
761
 
            revno = branch.revision_id_to_revno(revision_id)
762
 
        except errors.NoSuchRevision:
763
 
            revno = None
764
 
        return RevisionInfo(branch, revno, revision_id)
 
785
        return RevisionInfo(branch, None, revision_id)
765
786
 
766
787
    @staticmethod
767
788
    def _find_revision_id(branch, other_location):
821
842
                branch.fetch(other_branch, revision_b)
822
843
            except errors.ReadOnlyError:
823
844
                branch = other_branch
824
 
        try:
825
 
            revno = branch.revision_id_to_revno(revision_b)
826
 
        except errors.NoSuchRevision:
827
 
            revno = None
828
 
        return RevisionInfo(branch, revno, revision_b)
 
845
        return RevisionInfo(branch, None, revision_b)
829
846
 
830
847
    def _as_revision_id(self, context_branch):
831
848
        from bzrlib.branch import Branch
883
900
            location_type = 'parent branch'
884
901
        if submit_location is None:
885
902
            raise errors.NoSubmitBranch(branch)
886
 
        trace.note('Using %s %s', location_type, submit_location)
 
903
        trace.note(gettext('Using {0} {1}').format(location_type,
 
904
                                                        submit_location))
887
905
        return submit_location
888
906
 
889
907
    def _match_on(self, branch, revs):
896
914
            self._get_submit_location(context_branch))
897
915
 
898
916
 
 
917
class RevisionSpec_annotate(RevisionIDSpec):
 
918
 
 
919
    prefix = 'annotate:'
 
920
 
 
921
    help_txt = """Select the revision that last modified the specified line.
 
922
 
 
923
    Select the revision that last modified the specified line.  Line is
 
924
    specified as path:number.  Path is a relative path to the file.  Numbers
 
925
    start at 1, and are relative to the current version, not the last-
 
926
    committed version of the file.
 
927
    """
 
928
 
 
929
    def _raise_invalid(self, numstring, context_branch):
 
930
        raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
931
            'No such line: %s' % numstring)
 
932
 
 
933
    def _as_revision_id(self, context_branch):
 
934
        path, numstring = self.spec.rsplit(':', 1)
 
935
        try:
 
936
            index = int(numstring) - 1
 
937
        except ValueError:
 
938
            self._raise_invalid(numstring, context_branch)
 
939
        tree, file_path = workingtree.WorkingTree.open_containing(path)
 
940
        tree.lock_read()
 
941
        try:
 
942
            file_id = tree.path2id(file_path)
 
943
            if file_id is None:
 
944
                raise errors.InvalidRevisionSpec(self.user_spec,
 
945
                    context_branch, "File '%s' is not versioned." %
 
946
                    file_path)
 
947
            revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
 
948
        finally:
 
949
            tree.unlock()
 
950
        try:
 
951
            revision_id = revision_ids[index]
 
952
        except IndexError:
 
953
            self._raise_invalid(numstring, context_branch)
 
954
        if revision_id == revision.CURRENT_REVISION:
 
955
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
956
                'Line %s has not been committed.' % numstring)
 
957
        return revision_id
 
958
 
 
959
 
 
960
class RevisionSpec_mainline(RevisionIDSpec):
 
961
 
 
962
    help_txt = """Select mainline revision that merged the specified revision.
 
963
 
 
964
    Select the revision that merged the specified revision into mainline.
 
965
    """
 
966
 
 
967
    prefix = 'mainline:'
 
968
 
 
969
    def _as_revision_id(self, context_branch):
 
970
        revspec = RevisionSpec.from_string(self.spec)
 
971
        if revspec.get_branch() is None:
 
972
            spec_branch = context_branch
 
973
        else:
 
974
            spec_branch = _mod_branch.Branch.open(revspec.get_branch())
 
975
        revision_id = revspec.as_revision_id(spec_branch)
 
976
        graph = context_branch.repository.get_graph()
 
977
        result = graph.find_lefthand_merger(revision_id,
 
978
                                            context_branch.last_revision())
 
979
        if result is None:
 
980
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
 
981
        return result
 
982
 
 
983
 
899
984
# The order in which we want to DWIM a revision spec without any prefix.
900
985
# revno is always tried first and isn't listed here, this is used by
901
986
# RevisionSpec_dwim._match_on
902
 
dwim_revspecs = [
903
 
    RevisionSpec_tag, # Let's try for a tag
904
 
    RevisionSpec_revid, # Maybe it's a revid?
905
 
    RevisionSpec_date, # Perhaps a date?
906
 
    RevisionSpec_branch, # OK, last try, maybe it's a branch
907
 
    ]
 
987
dwim_revspecs = symbol_versioning.deprecated_list(
 
988
    symbol_versioning.deprecated_in((2, 4, 0)), "dwim_revspecs", [])
908
989
 
 
990
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_tag)
 
991
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_revid)
 
992
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_date)
 
993
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_branch)
909
994
 
910
995
revspec_registry = registry.Registry()
911
996
def _register_revspec(revspec):
920
1005
_register_revspec(RevisionSpec_ancestor)
921
1006
_register_revspec(RevisionSpec_branch)
922
1007
_register_revspec(RevisionSpec_submit)
923
 
 
924
 
# classes in this list should have a "prefix" attribute, against which
925
 
# string specs are matched
926
 
SPEC_TYPES = symbol_versioning.deprecated_list(
927
 
    symbol_versioning.deprecated_in((1, 12, 0)), "SPEC_TYPES", [])
 
1008
_register_revspec(RevisionSpec_annotate)
 
1009
_register_revspec(RevisionSpec_mainline)