1import hglib.client  # Circular dependency.
2from hglib import util, templates
3from hglib.error import CommandError
4from hglib.util import b, strtobytes, integertypes
5
6_nullcset = [b('-1'), b('0000000000000000000000000000000000000000'), b(''),
7             b(''), b(''), b(''), b('')]
8
9class changectx(object):
10    """A changecontext object makes access to data related to a particular
11    changeset convenient."""
12    def __init__(self, repo, changeid=b('')):
13        """changeid is a revision number, node, or tag"""
14        if changeid == b(''):
15            changeid = b('.')
16        self._repo = repo
17        if isinstance(changeid, hglib.client.revision):
18            cset = changeid
19        elif changeid == -1:
20            cset = _nullcset
21        else:
22            if isinstance(changeid, integertypes):
23                changeid = b('rev(') + strtobytes(changeid) + b(')')
24
25            notfound = False
26            try:
27                cset = self._repo.log(changeid)
28                # hg bbf4f3dfd700 gave a null result for tip+1
29                if (cset and cset[0][1] == _nullcset[1]
30                    and cset[0][0] != _nullcset[0]):
31                    notfound = True
32            except CommandError:
33                notfound = True
34
35            if notfound or not len(cset):
36                raise ValueError('changeid %r not found in repo' % changeid)
37            if len(cset) > 1:
38                raise ValueError('changeid must yield a single changeset')
39            cset = cset[0]
40
41        self._rev, self._node, self._tags = cset[:3]
42        self._branch, self._author, self._description, self._date = cset[3:]
43
44        self._rev = int(self._rev)
45
46        self._tags = self._tags.split()
47        try:
48            self._tags.remove(b('tip'))
49        except ValueError:
50            pass
51
52        self._ignored = None
53        self._clean = None
54
55    def __str__(self):
56        return self._node[:12].decode('latin-1')
57
58    def __int__(self):
59        return self._rev
60
61    def __repr__(self):
62        return "<changectx %s>" % str(self)
63
64    def __hash__(self):
65        try:
66            return hash(self._rev)
67        except AttributeError:
68            return id(self)
69
70    def __eq__(self, other):
71        try:
72            return self._rev == other._rev
73        except AttributeError:
74            return False
75
76    def __ne__(self, other):
77        return not (self == other)
78
79    def __nonzero__(self):
80        return self._rev != -1
81
82    def __bool__(self):
83        return self.__nonzero__()
84
85    def __contains__(self, key):
86        return key in self._manifest
87
88    def __iter__(self):
89        for f in sorted(self._manifest):
90            yield f
91
92    @util.propertycache
93    def _status(self):
94        return self._parsestatus(self._repo.status(change=strtobytes(self)))[:4]
95
96    def _parsestatus(self, stat):
97        d = dict((c, [])
98                 for c in (b('M'), b('A'), b('R'), b('!'), b('?'), b('I'),
99                    b('C'), b(' ')))
100        for k, path in stat:
101            d[k].append(path)
102        return (d[b('M')], d[b('A')], d[b('R')], d[b('!')], d[b('?')],
103                d[b('I')], d[b('C')])
104
105    def status(self, ignored=False, clean=False):
106        """Explicit status query
107        Unless this method is used to query the working copy status, the
108        _status property will implicitly read the status using its default
109        arguments."""
110        stat = self._parsestatus(self._repo.status(change=strtobytes(self),
111                                                   ignored=ignored,
112                                                   clean=clean))
113        self._unknown = self._ignored = self._clean = None
114        if ignored:
115            self._ignored = stat[5]
116        if clean:
117            self._clean = stat[6]
118        self._status = stat[:4]
119        return stat
120
121    def rev(self):
122        return self._rev
123
124    def node(self):
125        return self._node
126
127    def tags(self):
128        return self._tags
129
130    def branch(self):
131        return self._branch
132
133    def author(self):
134        return self._author
135
136    def user(self):
137        return self._author
138
139    def date(self):
140        return self._date
141
142    def description(self):
143        return self._description
144
145    def files(self):
146        return sorted(self._status[0] + self._status[1] + self._status[2])
147
148    def modified(self):
149        return self._status[0]
150
151    def added(self):
152        return self._status[1]
153
154    def removed(self):
155        return self._status[2]
156
157    def ignored(self):
158        if self._ignored is None:
159            self.status(ignored=True)
160        return self._ignored
161
162    def clean(self):
163        if self._clean is None:
164            self.status(clean=True)
165        return self._clean
166
167    @util.propertycache
168    def _manifest(self):
169        d = {}
170        for node, p, e, s, path in self._repo.manifest(rev=strtobytes(self)):
171            d[path] = node
172        return d
173
174    def manifest(self):
175        return self._manifest
176
177    def hex(self):
178        return hex(self._node)
179
180    @util.propertycache
181    def _parents(self):
182        """return contexts for each parent changeset"""
183        par = self._repo.parents(rev=strtobytes(self))
184        if not par:
185            return [changectx(self._repo, -1)]
186        return [changectx(self._repo, int(cset.rev)) for cset in par]
187
188    def parents(self):
189        return self._parents
190
191    def p1(self):
192        return self._parents[0]
193
194    def p2(self):
195        if len(self._parents) == 2:
196            return self._parents[1]
197        return changectx(self._repo, -1)
198
199    @util.propertycache
200    def _bookmarks(self):
201        books = [bm for bm in self._repo.bookmarks()[0] if bm[1] == self._rev]
202
203        bms = []
204        for name, r, n in books:
205            bms.append(name)
206        return bms
207
208    def bookmarks(self):
209        return self._bookmarks
210
211    def hidden(self):
212        """return True if the changeset is hidden, else False"""
213        return bool(self._repo.log(revrange=self._node + b(' and hidden()'),
214                                   hidden=True))
215
216    def phase(self):
217        """return the phase of the changeset (public, draft or secret)"""
218        return self._repo.phase(strtobytes(self._rev))[0][1]
219
220    def children(self):
221        """return contexts for each child changeset"""
222        for c in self._repo.log(b('children(') + self._node + b(')')):
223            yield changectx(self._repo, c)
224
225    def ancestors(self):
226        for a in self._repo.log(b('ancestors(') + self._node + b(')')):
227            yield changectx(self._repo, a)
228
229    def descendants(self):
230        for d in self._repo.log(b('descendants(') + self._node + b(')')):
231            yield changectx(self._repo, d)
232
233    def ancestor(self, c2):
234        """
235        return the ancestor context of self and c2
236        """
237        return changectx(self._repo,
238                         b('ancestor(') + self + b(', ') + c2 + b(')'))
239