1# Copyright (C) 2005-2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17# TODO: Some kind of command-line display of revision properties:
18# perhaps show them in log -v and allow them as options to the commit command.
19
20
21from .lazy_import import lazy_import
22lazy_import(globals(), """
23from breezy import bugtracker
24""")
25from . import (
26    errors,
27    osutils,
28    )
29
30NULL_REVISION = b"null:"
31CURRENT_REVISION = b"current:"
32
33
34class Revision(object):
35    """Single revision on a branch.
36
37    Revisions may know their revision_hash, but only once they've been
38    written out.  This is not stored because you cannot write the hash
39    into the file it describes.
40
41    After bzr 0.0.5 revisions are allowed to have multiple parents.
42
43    parent_ids
44        List of parent revision_ids
45
46    properties
47        Dictionary of revision properties.  These are attached to the
48        revision as extra metadata.  The name must be a single
49        word; the value can be an arbitrary string.
50    """
51
52    def __init__(self, revision_id, properties=None, **args):
53        self.revision_id = revision_id
54        if properties is None:
55            self.properties = {}
56        else:
57            self.properties = properties
58            self._check_properties()
59        self.committer = None
60        self.parent_ids = []
61        self.parent_sha1s = []
62        """Not used anymore - legacy from for 4."""
63        self.__dict__.update(args)
64
65    def __repr__(self):
66        return "<Revision id %s>" % self.revision_id
67
68    def __eq__(self, other):
69        if not isinstance(other, Revision):
70            return False
71        return (
72            self.inventory_sha1 == other.inventory_sha1
73            and self.revision_id == other.revision_id
74            and self.timestamp == other.timestamp
75            and self.message == other.message
76            and self.timezone == other.timezone
77            and self.committer == other.committer
78            and self.properties == other.properties
79            and self.parent_ids == other.parent_ids)
80
81    def __ne__(self, other):
82        return not self.__eq__(other)
83
84    def _check_properties(self):
85        """Verify that all revision properties are OK."""
86        for name, value in self.properties.items():
87            # GZ 2017-06-10: What sort of string are properties exactly?
88            not_text = not isinstance(name, str)
89            if not_text or osutils.contains_whitespace(name):
90                raise ValueError("invalid property name %r" % name)
91            if not isinstance(value, (str, bytes)):
92                raise ValueError("invalid property value %r for %r" %
93                                 (value, name))
94
95    def get_history(self, repository):
96        """Return the canonical line-of-history for this revision.
97
98        If ghosts are present this may differ in result from a ghost-free
99        repository.
100        """
101        current_revision = self
102        reversed_result = []
103        while current_revision is not None:
104            reversed_result.append(current_revision.revision_id)
105            if not len(current_revision.parent_ids):
106                reversed_result.append(None)
107                current_revision = None
108            else:
109                next_revision_id = current_revision.parent_ids[0]
110                current_revision = repository.get_revision(next_revision_id)
111        reversed_result.reverse()
112        return reversed_result
113
114    def get_summary(self):
115        """Get the first line of the log message for this revision.
116
117        Return an empty string if message is None.
118        """
119        if self.message:
120            return self.message.lstrip().split('\n', 1)[0]
121        else:
122            return ''
123
124    def get_apparent_authors(self):
125        """Return the apparent authors of this revision.
126
127        If the revision properties contain the names of the authors,
128        return them. Otherwise return the committer name.
129
130        The return value will be a list containing at least one element.
131        """
132        authors = self.properties.get('authors', None)
133        if authors is None:
134            author = self.properties.get('author', self.committer)
135            if author is None:
136                return []
137            return [author]
138        else:
139            return authors.split("\n")
140
141    def iter_bugs(self):
142        """Iterate over the bugs associated with this revision."""
143        bug_property = self.properties.get('bugs', None)
144        if bug_property is None:
145            return iter([])
146        return bugtracker.decode_bug_urls(bug_property)
147
148
149def iter_ancestors(revision_id, revision_source, only_present=False):
150    ancestors = (revision_id,)
151    distance = 0
152    while len(ancestors) > 0:
153        new_ancestors = []
154        for ancestor in ancestors:
155            if not only_present:
156                yield ancestor, distance
157            try:
158                revision = revision_source.get_revision(ancestor)
159            except errors.NoSuchRevision as e:
160                if e.revision == revision_id:
161                    raise
162                else:
163                    continue
164            if only_present:
165                yield ancestor, distance
166            new_ancestors.extend(revision.parent_ids)
167        ancestors = new_ancestors
168        distance += 1
169
170
171def find_present_ancestors(revision_id, revision_source):
172    """Return the ancestors of a revision present in a branch.
173
174    It's possible that a branch won't have the complete ancestry of
175    one of its revisions.
176
177    """
178    found_ancestors = {}
179    anc_iter = enumerate(iter_ancestors(revision_id, revision_source,
180                                        only_present=True))
181    for anc_order, (anc_id, anc_distance) in anc_iter:
182        if anc_id not in found_ancestors:
183            found_ancestors[anc_id] = (anc_order, anc_distance)
184    return found_ancestors
185
186
187def __get_closest(intersection):
188    intersection.sort()
189    matches = []
190    for entry in intersection:
191        if entry[0] == intersection[0][0]:
192            matches.append(entry[2])
193    return matches
194
195
196def is_reserved_id(revision_id):
197    """Determine whether a revision id is reserved
198
199    :return: True if the revision is reserved, False otherwise
200    """
201    return isinstance(revision_id, bytes) and revision_id.endswith(b':')
202
203
204def check_not_reserved_id(revision_id):
205    """Raise ReservedId if the supplied revision_id is reserved"""
206    if is_reserved_id(revision_id):
207        raise errors.ReservedId(revision_id)
208
209
210def ensure_null(revision_id):
211    """Ensure only NULL_REVISION is used to represent the null revision"""
212    if revision_id is None:
213        raise ValueError(
214            'NULL_REVISION should be used for the null'
215            ' revision instead of None.')
216    return revision_id
217
218
219def is_null(revision_id):
220    if revision_id is None:
221        raise ValueError('NULL_REVISION should be used for the null'
222                         ' revision instead of None.')
223    return (revision_id == NULL_REVISION)
224