##// END OF EJS Templates
vcs: access changeset status "struct" members - hg 5.3 will drop indexed access
Mads Kiilerich -
r8051:744b7e78 default
parent child Browse files
Show More
@@ -1,407 +1,407 b''
1 1 import os
2 2 import posixpath
3 3
4 4 import mercurial.archival
5 5 import mercurial.node
6 6 import mercurial.obsutil
7 7
8 8 from kallithea.lib.vcs.backends.base import BaseChangeset
9 9 from kallithea.lib.vcs.conf import settings
10 10 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
11 11 from kallithea.lib.vcs.nodes import (
12 12 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode)
13 13 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_bytes, safe_str, safe_unicode
14 14 from kallithea.lib.vcs.utils.lazy import LazyProperty
15 15 from kallithea.lib.vcs.utils.paths import get_dirs_for_path
16 16
17 17
18 18 class MercurialChangeset(BaseChangeset):
19 19 """
20 20 Represents state of the repository at a revision.
21 21 """
22 22
23 23 def __init__(self, repository, revision):
24 24 self.repository = repository
25 25 assert isinstance(revision, basestring), repr(revision)
26 26 self._ctx = repository._repo[ascii_bytes(revision)]
27 27 self.raw_id = ascii_str(self._ctx.hex())
28 28 self.revision = self._ctx._rev
29 29 self.nodes = {}
30 30
31 31 @LazyProperty
32 32 def tags(self):
33 33 return [safe_unicode(tag) for tag in self._ctx.tags()]
34 34
35 35 @LazyProperty
36 36 def branch(self):
37 37 return safe_unicode(self._ctx.branch())
38 38
39 39 @LazyProperty
40 40 def branches(self):
41 41 return [safe_unicode(self._ctx.branch())]
42 42
43 43 @LazyProperty
44 44 def closesbranch(self):
45 45 return self._ctx.closesbranch()
46 46
47 47 @LazyProperty
48 48 def obsolete(self):
49 49 return self._ctx.obsolete()
50 50
51 51 @LazyProperty
52 52 def bumped(self):
53 53 return self._ctx.phasedivergent()
54 54
55 55 @LazyProperty
56 56 def divergent(self):
57 57 return self._ctx.contentdivergent()
58 58
59 59 @LazyProperty
60 60 def extinct(self):
61 61 return self._ctx.extinct()
62 62
63 63 @LazyProperty
64 64 def unstable(self):
65 65 return self._ctx.orphan()
66 66
67 67 @LazyProperty
68 68 def phase(self):
69 69 if(self._ctx.phase() == 1):
70 70 return 'Draft'
71 71 elif(self._ctx.phase() == 2):
72 72 return 'Secret'
73 73 else:
74 74 return ''
75 75
76 76 @LazyProperty
77 77 def successors(self):
78 78 successors = mercurial.obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True)
79 79 if successors:
80 80 # flatten the list here handles both divergent (len > 1)
81 81 # and the usual case (len = 1)
82 82 successors = [mercurial.node.hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()]
83 83
84 84 return successors
85 85
86 86 @LazyProperty
87 87 def predecessors(self):
88 88 return [mercurial.node.hex(n)[:12] for n in mercurial.obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())]
89 89
90 90 @LazyProperty
91 91 def bookmarks(self):
92 92 return [safe_unicode(bookmark) for bookmark in self._ctx.bookmarks()]
93 93
94 94 @LazyProperty
95 95 def message(self):
96 96 return safe_unicode(self._ctx.description())
97 97
98 98 @LazyProperty
99 99 def committer(self):
100 100 return safe_unicode(self.author)
101 101
102 102 @LazyProperty
103 103 def author(self):
104 104 return safe_unicode(self._ctx.user())
105 105
106 106 @LazyProperty
107 107 def date(self):
108 108 return date_fromtimestamp(*self._ctx.date())
109 109
110 110 @LazyProperty
111 111 def _timestamp(self):
112 112 return self._ctx.date()[0]
113 113
114 114 @LazyProperty
115 115 def status(self):
116 116 """
117 117 Returns modified, added, removed, deleted files for current changeset
118 118 """
119 119 return self.repository._repo.status(self._ctx.p1().node(),
120 120 self._ctx.node())
121 121
122 122 @LazyProperty
123 123 def _file_paths(self):
124 124 return list(self._ctx)
125 125
126 126 @LazyProperty
127 127 def _dir_paths(self):
128 128 p = list(set(get_dirs_for_path(*self._file_paths)))
129 129 p.insert(0, '')
130 130 return p
131 131
132 132 @LazyProperty
133 133 def _paths(self):
134 134 return self._dir_paths + self._file_paths
135 135
136 136 @LazyProperty
137 137 def short_id(self):
138 138 return self.raw_id[:12]
139 139
140 140 @LazyProperty
141 141 def parents(self):
142 142 """
143 143 Returns list of parents changesets.
144 144 """
145 145 return [self.repository.get_changeset(parent.rev())
146 146 for parent in self._ctx.parents() if parent.rev() >= 0]
147 147
148 148 @LazyProperty
149 149 def children(self):
150 150 """
151 151 Returns list of children changesets.
152 152 """
153 153 return [self.repository.get_changeset(child.rev())
154 154 for child in self._ctx.children() if child.rev() >= 0]
155 155
156 156 def next(self, branch=None):
157 157 if branch and self.branch != branch:
158 158 raise VCSError('Branch option used on changeset not belonging '
159 159 'to that branch')
160 160
161 161 cs = self
162 162 while True:
163 163 try:
164 164 next_ = cs.repository.revisions.index(cs.raw_id) + 1
165 165 next_rev = cs.repository.revisions[next_]
166 166 except IndexError:
167 167 raise ChangesetDoesNotExistError
168 168 cs = cs.repository.get_changeset(next_rev)
169 169
170 170 if not branch or branch == cs.branch:
171 171 return cs
172 172
173 173 def prev(self, branch=None):
174 174 if branch and self.branch != branch:
175 175 raise VCSError('Branch option used on changeset not belonging '
176 176 'to that branch')
177 177
178 178 cs = self
179 179 while True:
180 180 try:
181 181 prev_ = cs.repository.revisions.index(cs.raw_id) - 1
182 182 if prev_ < 0:
183 183 raise IndexError
184 184 prev_rev = cs.repository.revisions[prev_]
185 185 except IndexError:
186 186 raise ChangesetDoesNotExistError
187 187 cs = cs.repository.get_changeset(prev_rev)
188 188
189 189 if not branch or branch == cs.branch:
190 190 return cs
191 191
192 192 def diff(self):
193 193 # Only used to feed diffstat
194 194 return b''.join(self._ctx.diff())
195 195
196 196 def _fix_path(self, path):
197 197 """
198 198 Paths are stored without trailing slash so we need to get rid off it if
199 199 needed. Also mercurial keeps filenodes as str so we need to decode
200 200 from unicode to str
201 201 """
202 202 if path.endswith('/'):
203 203 path = path.rstrip('/')
204 204
205 205 return safe_str(path)
206 206
207 207 def _get_kind(self, path):
208 208 path = self._fix_path(path)
209 209 if path in self._file_paths:
210 210 return NodeKind.FILE
211 211 elif path in self._dir_paths:
212 212 return NodeKind.DIR
213 213 else:
214 214 raise ChangesetError("Node does not exist at the given path '%s'"
215 215 % (path))
216 216
217 217 def _get_filectx(self, path):
218 218 path = self._fix_path(path)
219 219 if self._get_kind(path) != NodeKind.FILE:
220 220 raise ChangesetError("File does not exist for revision %s at "
221 221 " '%s'" % (self.raw_id, path))
222 222 return self._ctx.filectx(safe_bytes(path))
223 223
224 224 def _extract_submodules(self):
225 225 """
226 226 returns a dictionary with submodule information from substate file
227 227 of hg repository
228 228 """
229 229 return self._ctx.substate
230 230
231 231 def get_file_mode(self, path):
232 232 """
233 233 Returns stat mode of the file at the given ``path``.
234 234 """
235 235 fctx = self._get_filectx(path)
236 236 if b'x' in fctx.flags():
237 237 return 0o100755
238 238 else:
239 239 return 0o100644
240 240
241 241 def get_file_content(self, path):
242 242 """
243 243 Returns content of the file at given ``path``.
244 244 """
245 245 fctx = self._get_filectx(path)
246 246 return fctx.data()
247 247
248 248 def get_file_size(self, path):
249 249 """
250 250 Returns size of the file at given ``path``.
251 251 """
252 252 fctx = self._get_filectx(path)
253 253 return fctx.size()
254 254
255 255 def get_file_changeset(self, path):
256 256 """
257 257 Returns last commit of the file at the given ``path``.
258 258 """
259 259 return self.get_file_history(path, limit=1)[0]
260 260
261 261 def get_file_history(self, path, limit=None):
262 262 """
263 263 Returns history of file as reversed list of ``Changeset`` objects for
264 264 which file at given ``path`` has been modified.
265 265 """
266 266 fctx = self._get_filectx(path)
267 267 hist = []
268 268 cnt = 0
269 269 for cs in reversed([x for x in fctx.filelog()]):
270 270 cnt += 1
271 271 hist.append(mercurial.node.hex(fctx.filectx(cs).node()))
272 272 if limit is not None and cnt == limit:
273 273 break
274 274
275 275 return [self.repository.get_changeset(node) for node in hist]
276 276
277 277 def get_file_annotate(self, path):
278 278 """
279 279 Returns a generator of four element tuples with
280 280 lineno, sha, changeset lazy loader and line
281 281 """
282 282 annotations = self._get_filectx(path).annotate()
283 283 annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations]
284 284 for i, (fctx, line) in enumerate(annotation_lines):
285 285 sha = ascii_str(fctx.hex())
286 286 yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line)
287 287
288 288 def fill_archive(self, stream=None, kind='tgz', prefix=None,
289 289 subrepos=False):
290 290 """
291 291 Fills up given stream.
292 292
293 293 :param stream: file like object.
294 294 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
295 295 Default: ``tgz``.
296 296 :param prefix: name of root directory in archive.
297 297 Default is repository name and changeset's raw_id joined with dash
298 298 (``repo-tip.<KIND>``).
299 299 :param subrepos: include subrepos in this archive.
300 300
301 301 :raise ImproperArchiveTypeError: If given kind is wrong.
302 302 :raise VcsError: If given stream is None
303 303 """
304 304 allowed_kinds = settings.ARCHIVE_SPECS
305 305 if kind not in allowed_kinds:
306 306 raise ImproperArchiveTypeError('Archive kind not supported use one'
307 307 'of %s' % ' '.join(allowed_kinds))
308 308
309 309 if stream is None:
310 310 raise VCSError('You need to pass in a valid stream for filling'
311 311 ' with archival data')
312 312
313 313 if prefix is None:
314 314 prefix = '%s-%s' % (self.repository.name, self.short_id)
315 315 elif prefix.startswith('/'):
316 316 raise VCSError("Prefix cannot start with leading slash")
317 317 elif prefix.strip() == '':
318 318 raise VCSError("Prefix cannot be empty")
319 319
320 320 mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id),
321 321 safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos)
322 322
323 323 def get_nodes(self, path):
324 324 """
325 325 Returns combined ``DirNode`` and ``FileNode`` objects list representing
326 326 state of changeset at the given ``path``. If node at the given ``path``
327 327 is not instance of ``DirNode``, ChangesetError would be raised.
328 328 """
329 329
330 330 if self._get_kind(path) != NodeKind.DIR:
331 331 raise ChangesetError("Directory does not exist for revision %s at "
332 332 " '%s'" % (self.revision, path))
333 333 path = self._fix_path(path)
334 334
335 335 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
336 336 if os.path.dirname(f) == path]
337 337 dirs = path == '' and '' or [d for d in self._dir_paths
338 338 if d and posixpath.dirname(d) == path]
339 339 dirnodes = [DirNode(d, changeset=self) for d in dirs
340 340 if os.path.dirname(d) == path]
341 341
342 342 als = self.repository.alias
343 343 for k, vals in self._extract_submodules().iteritems():
344 344 #vals = url,rev,type
345 345 loc = vals[0]
346 346 cs = vals[1]
347 347 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
348 348 alias=als))
349 349 nodes = dirnodes + filenodes
350 350 for node in nodes:
351 351 self.nodes[node.path] = node
352 352 nodes.sort()
353 353 return nodes
354 354
355 355 def get_node(self, path):
356 356 """
357 357 Returns ``Node`` object from the given ``path``. If there is no node at
358 358 the given ``path``, ``ChangesetError`` would be raised.
359 359 """
360 360 path = self._fix_path(path)
361 361 if path not in self.nodes:
362 362 if path in self._file_paths:
363 363 node = FileNode(path, changeset=self)
364 364 elif path in self._dir_paths or path in self._dir_paths:
365 365 if path == '':
366 366 node = RootNode(changeset=self)
367 367 else:
368 368 node = DirNode(path, changeset=self)
369 369 else:
370 370 raise NodeDoesNotExistError("There is no file nor directory "
371 371 "at the given path: '%s' at revision %s"
372 372 % (path, self.short_id))
373 373 # cache node
374 374 self.nodes[path] = node
375 375 return self.nodes[path]
376 376
377 377 @LazyProperty
378 378 def affected_files(self):
379 379 """
380 380 Gets a fast accessible file changes for given changeset
381 381 """
382 382 return self._ctx.files()
383 383
384 384 @property
385 385 def added(self):
386 386 """
387 387 Returns list of added ``FileNode`` objects.
388 388 """
389 return AddedFileNodesGenerator([n for n in self.status[1]], self)
389 return AddedFileNodesGenerator([n for n in self.status.added], self)
390 390
391 391 @property
392 392 def changed(self):
393 393 """
394 394 Returns list of modified ``FileNode`` objects.
395 395 """
396 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
396 return ChangedFileNodesGenerator([n for n in self.status.modified], self)
397 397
398 398 @property
399 399 def removed(self):
400 400 """
401 401 Returns list of removed ``FileNode`` objects.
402 402 """
403 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
403 return RemovedFileNodesGenerator([n for n in self.status.removed], self)
404 404
405 405 @LazyProperty
406 406 def extra(self):
407 407 return self._ctx.extra()
General Comments 0
You need to be logged in to leave comments. Login now