##// END OF EJS Templates
Add support for vcs Changeset objects in get_diff method
marcink -
r2386:ed8c2fc8 beta
parent child Browse files
Show More
@@ -1,521 +1,527 b''
1 1 import os
2 2 import time
3 3 import datetime
4 4 import urllib
5 5 import urllib2
6 6
7 7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 8 from .workdir import MercurialWorkdir
9 9 from .changeset import MercurialChangeset
10 10 from .inmemory import MercurialInMemoryChangeset
11 11
12 12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 16 makedate, safe_unicode
17 17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 19 from rhodecode.lib.vcs.utils.paths import abspath
20 20
21 21 from ...utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23 23
24 24
25 25 class MercurialRepository(BaseRepository):
26 26 """
27 27 Mercurial repository backend
28 28 """
29 29 DEFAULT_BRANCH_NAME = 'default'
30 30 scm = 'hg'
31 31
32 32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 33 update_after_clone=False):
34 34 """
35 35 Raises RepositoryError if repository could not be find at the given
36 36 ``repo_path``.
37 37
38 38 :param repo_path: local path of the repository
39 39 :param create=False: if set to True, would try to create repository if
40 40 it does not exist rather than raising exception
41 41 :param baseui=None: user data
42 42 :param src_url=None: would try to clone repository from given location
43 43 :param update_after_clone=False: sets update of working copy after
44 44 making a clone
45 45 """
46 46
47 47 if not isinstance(repo_path, str):
48 48 raise VCSError('Mercurial backend requires repository path to '
49 49 'be instance of <str> got %s instead' %
50 50 type(repo_path))
51 51
52 52 self.path = abspath(repo_path)
53 53 self.baseui = baseui or ui.ui()
54 54 # We've set path and ui, now we can set _repo itself
55 55 self._repo = self._get_repo(create, src_url, update_after_clone)
56 56
57 57 @property
58 58 def _empty(self):
59 59 """
60 60 Checks if repository is empty without any changesets
61 61 """
62 62 # TODO: Following raises errors when using InMemoryChangeset...
63 63 # return len(self._repo.changelog) == 0
64 64 return len(self.revisions) == 0
65 65
66 66 @LazyProperty
67 67 def revisions(self):
68 68 """
69 69 Returns list of revisions' ids, in ascending order. Being lazy
70 70 attribute allows external tools to inject shas from cache.
71 71 """
72 72 return self._get_all_revisions()
73 73
74 74 @LazyProperty
75 75 def name(self):
76 76 return os.path.basename(self.path)
77 77
78 78 @LazyProperty
79 79 def branches(self):
80 80 return self._get_branches()
81 81
82 82 def _get_branches(self, closed=False):
83 83 """
84 84 Get's branches for this repository
85 85 Returns only not closed branches by default
86 86
87 87 :param closed: return also closed branches for mercurial
88 88 """
89 89
90 90 if self._empty:
91 91 return {}
92 92
93 93 def _branchtags(localrepo):
94 94 """
95 95 Patched version of mercurial branchtags to not return the closed
96 96 branches
97 97
98 98 :param localrepo: locarepository instance
99 99 """
100 100
101 101 bt = {}
102 102 bt_closed = {}
103 103 for bn, heads in localrepo.branchmap().iteritems():
104 104 tip = heads[-1]
105 105 if 'close' in localrepo.changelog.read(tip)[5]:
106 106 bt_closed[bn] = tip
107 107 else:
108 108 bt[bn] = tip
109 109
110 110 if closed:
111 111 bt.update(bt_closed)
112 112 return bt
113 113
114 114 sortkey = lambda ctx: ctx[0] # sort by name
115 115 _branches = [(safe_unicode(n), hex(h),) for n, h in
116 116 _branchtags(self._repo).items()]
117 117
118 118 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
119 119
120 120 @LazyProperty
121 121 def tags(self):
122 122 """
123 123 Get's tags for this repository
124 124 """
125 125 return self._get_tags()
126 126
127 127 def _get_tags(self):
128 128 if self._empty:
129 129 return {}
130 130
131 131 sortkey = lambda ctx: ctx[0] # sort by name
132 132 _tags = [(safe_unicode(n), hex(h),) for n, h in
133 133 self._repo.tags().items()]
134 134
135 135 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
136 136
137 137 def tag(self, name, user, revision=None, message=None, date=None,
138 138 **kwargs):
139 139 """
140 140 Creates and returns a tag for the given ``revision``.
141 141
142 142 :param name: name for new tag
143 143 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
144 144 :param revision: changeset id for which new tag would be created
145 145 :param message: message of the tag's commit
146 146 :param date: date of tag's commit
147 147
148 148 :raises TagAlreadyExistError: if tag with same name already exists
149 149 """
150 150 if name in self.tags:
151 151 raise TagAlreadyExistError("Tag %s already exists" % name)
152 152 changeset = self.get_changeset(revision)
153 153 local = kwargs.setdefault('local', False)
154 154
155 155 if message is None:
156 156 message = "Added tag %s for changeset %s" % (name,
157 157 changeset.short_id)
158 158
159 159 if date is None:
160 160 date = datetime.datetime.now().ctime()
161 161
162 162 try:
163 163 self._repo.tag(name, changeset._ctx.node(), message, local, user,
164 164 date)
165 165 except Abort, e:
166 166 raise RepositoryError(e.message)
167 167
168 168 # Reinitialize tags
169 169 self.tags = self._get_tags()
170 170 tag_id = self.tags[name]
171 171
172 172 return self.get_changeset(revision=tag_id)
173 173
174 174 def remove_tag(self, name, user, message=None, date=None):
175 175 """
176 176 Removes tag with the given ``name``.
177 177
178 178 :param name: name of the tag to be removed
179 179 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
180 180 :param message: message of the tag's removal commit
181 181 :param date: date of tag's removal commit
182 182
183 183 :raises TagDoesNotExistError: if tag with given name does not exists
184 184 """
185 185 if name not in self.tags:
186 186 raise TagDoesNotExistError("Tag %s does not exist" % name)
187 187 if message is None:
188 188 message = "Removed tag %s" % name
189 189 if date is None:
190 190 date = datetime.datetime.now().ctime()
191 191 local = False
192 192
193 193 try:
194 194 self._repo.tag(name, nullid, message, local, user, date)
195 195 self.tags = self._get_tags()
196 196 except Abort, e:
197 197 raise RepositoryError(e.message)
198 198
199 199 @LazyProperty
200 200 def bookmarks(self):
201 201 """
202 202 Get's bookmarks for this repository
203 203 """
204 204 return self._get_bookmarks()
205 205
206 206 def _get_bookmarks(self):
207 207 if self._empty:
208 208 return {}
209 209
210 210 sortkey = lambda ctx: ctx[0] # sort by name
211 211 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
212 212 self._repo._bookmarks.items()]
213 213 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
214 214
215 215 def _get_all_revisions(self):
216 216
217 217 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
218 218
219 219 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
220 220 context=3):
221 221 """
222 222 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 223 ``rev2`` since ``rev1``.
224 224
225 225 :param rev1: Entry point from which diff is shown. Can be
226 226 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 227 the changes since empty state of the repository until ``rev2``
228 228 :param rev2: Until which revision changes should be shown.
229 229 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 230 changes. Defaults to ``False``.
231 231 :param context: How many lines before/after changed lines should be
232 232 shown. Defaults to ``3``.
233 233 """
234 if hasattr(rev1, 'raw_id'):
235 rev1 = getattr(rev1, 'raw_id')
236
237 if hasattr(rev2, 'raw_id'):
238 rev2 = getattr(rev2, 'raw_id')
239
234 240 # Check if given revisions are present at repository (may raise
235 241 # ChangesetDoesNotExistError)
236 242 if rev1 != self.EMPTY_CHANGESET:
237 243 self.get_changeset(rev1)
238 244 self.get_changeset(rev2)
239 245
240 246 file_filter = match(self.path, '', [path])
241 247 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
242 248 opts=diffopts(git=True,
243 249 ignorews=ignore_whitespace,
244 250 context=context)))
245 251
246 252 def _check_url(self, url):
247 253 """
248 254 Function will check given url and try to verify if it's a valid
249 255 link. Sometimes it may happened that mercurial will issue basic
250 256 auth request that can cause whole API to hang when used from python
251 257 or other external calls.
252 258
253 259 On failures it'll raise urllib2.HTTPError, return code 200 if url
254 260 is valid or True if it's a local path
255 261 """
256 262
257 263 from mercurial.util import url as Url
258 264
259 265 # those authnadlers are patched for python 2.6.5 bug an
260 266 # infinit looping when given invalid resources
261 267 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
262 268
263 269 # check first if it's not an local url
264 270 if os.path.isdir(url) or url.startswith('file:'):
265 271 return True
266 272
267 273 handlers = []
268 274 test_uri, authinfo = Url(url).authinfo()
269 275
270 276 if authinfo:
271 277 #create a password manager
272 278 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
273 279 passmgr.add_password(*authinfo)
274 280
275 281 handlers.extend((httpbasicauthhandler(passmgr),
276 282 httpdigestauthhandler(passmgr)))
277 283
278 284 o = urllib2.build_opener(*handlers)
279 285 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
280 286 ('Accept', 'application/mercurial-0.1')]
281 287
282 288 q = {"cmd": 'between'}
283 289 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
284 290 qs = '?%s' % urllib.urlencode(q)
285 291 cu = "%s%s" % (test_uri, qs)
286 292 req = urllib2.Request(cu, None, {})
287 293
288 294 try:
289 295 resp = o.open(req)
290 296 return resp.code == 200
291 297 except Exception, e:
292 298 # means it cannot be cloned
293 299 raise urllib2.URLError(e)
294 300
295 301 def _get_repo(self, create, src_url=None, update_after_clone=False):
296 302 """
297 303 Function will check for mercurial repository in given path and return
298 304 a localrepo object. If there is no repository in that path it will
299 305 raise an exception unless ``create`` parameter is set to True - in
300 306 that case repository would be created and returned.
301 307 If ``src_url`` is given, would try to clone repository from the
302 308 location at given clone_point. Additionally it'll make update to
303 309 working copy accordingly to ``update_after_clone`` flag
304 310 """
305 311 try:
306 312 if src_url:
307 313 url = str(self._get_url(src_url))
308 314 opts = {}
309 315 if not update_after_clone:
310 316 opts.update({'noupdate': True})
311 317 try:
312 318 self._check_url(url)
313 319 clone(self.baseui, url, self.path, **opts)
314 320 # except urllib2.URLError:
315 321 # raise Abort("Got HTTP 404 error")
316 322 except Exception:
317 323 raise
318 324 # Don't try to create if we've already cloned repo
319 325 create = False
320 326 return localrepository(self.baseui, self.path, create=create)
321 327 except (Abort, RepoError), err:
322 328 if create:
323 329 msg = "Cannot create repository at %s. Original error was %s"\
324 330 % (self.path, err)
325 331 else:
326 332 msg = "Not valid repository at %s. Original error was %s"\
327 333 % (self.path, err)
328 334 raise RepositoryError(msg)
329 335
330 336 @LazyProperty
331 337 def in_memory_changeset(self):
332 338 return MercurialInMemoryChangeset(self)
333 339
334 340 @LazyProperty
335 341 def description(self):
336 342 undefined_description = u'unknown'
337 343 return safe_unicode(self._repo.ui.config('web', 'description',
338 344 undefined_description, untrusted=True))
339 345
340 346 @LazyProperty
341 347 def contact(self):
342 348 undefined_contact = u'Unknown'
343 349 return safe_unicode(get_contact(self._repo.ui.config)
344 350 or undefined_contact)
345 351
346 352 @LazyProperty
347 353 def last_change(self):
348 354 """
349 355 Returns last change made on this repository as datetime object
350 356 """
351 357 return date_fromtimestamp(self._get_mtime(), makedate()[1])
352 358
353 359 def _get_mtime(self):
354 360 try:
355 361 return time.mktime(self.get_changeset().date.timetuple())
356 362 except RepositoryError:
357 363 #fallback to filesystem
358 364 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
359 365 st_path = os.path.join(self.path, '.hg', "store")
360 366 if os.path.exists(cl_path):
361 367 return os.stat(cl_path).st_mtime
362 368 else:
363 369 return os.stat(st_path).st_mtime
364 370
365 371 def _get_hidden(self):
366 372 return self._repo.ui.configbool("web", "hidden", untrusted=True)
367 373
368 374 def _get_revision(self, revision):
369 375 """
370 376 Get's an ID revision given as str. This will always return a fill
371 377 40 char revision number
372 378
373 379 :param revision: str or int or None
374 380 """
375 381
376 382 if self._empty:
377 383 raise EmptyRepositoryError("There are no changesets yet")
378 384
379 385 if revision in [-1, 'tip', None]:
380 386 revision = 'tip'
381 387
382 388 try:
383 389 revision = hex(self._repo.lookup(revision))
384 390 except (IndexError, ValueError, RepoLookupError, TypeError):
385 391 raise ChangesetDoesNotExistError("Revision %r does not "
386 392 "exist for this repository %s" \
387 393 % (revision, self))
388 394 return revision
389 395
390 396 def _get_archives(self, archive_name='tip'):
391 397 allowed = self.baseui.configlist("web", "allow_archive",
392 398 untrusted=True)
393 399 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
394 400 if i[0] in allowed or self._repo.ui.configbool("web",
395 401 "allow" + i[0],
396 402 untrusted=True):
397 403 yield {"type": i[0], "extension": i[1], "node": archive_name}
398 404
399 405 def _get_url(self, url):
400 406 """
401 407 Returns normalized url. If schema is not given, would fall
402 408 to filesystem
403 409 (``file:///``) schema.
404 410 """
405 411 url = str(url)
406 412 if url != 'default' and not '://' in url:
407 413 url = "file:" + urllib.pathname2url(url)
408 414 return url
409 415
410 416 def get_changeset(self, revision=None):
411 417 """
412 418 Returns ``MercurialChangeset`` object representing repository's
413 419 changeset at the given ``revision``.
414 420 """
415 421 revision = self._get_revision(revision)
416 422 changeset = MercurialChangeset(repository=self, revision=revision)
417 423 return changeset
418 424
419 425 def get_changesets(self, start=None, end=None, start_date=None,
420 426 end_date=None, branch_name=None, reverse=False):
421 427 """
422 428 Returns iterator of ``MercurialChangeset`` objects from start to end
423 429 (both are inclusive)
424 430
425 431 :param start: None, str, int or mercurial lookup format
426 432 :param end: None, str, int or mercurial lookup format
427 433 :param start_date:
428 434 :param end_date:
429 435 :param branch_name:
430 436 :param reversed: return changesets in reversed order
431 437 """
432 438
433 439 start_raw_id = self._get_revision(start)
434 440 start_pos = self.revisions.index(start_raw_id) if start else None
435 441 end_raw_id = self._get_revision(end)
436 442 end_pos = self.revisions.index(end_raw_id) if end else None
437 443
438 444 if None not in [start, end] and start_pos > end_pos:
439 445 raise RepositoryError("start revision '%s' cannot be "
440 446 "after end revision '%s'" % (start, end))
441 447
442 448 if branch_name and branch_name not in self.branches.keys():
443 449 raise BranchDoesNotExistError('Such branch %s does not exists for'
444 450 ' this repository' % branch_name)
445 451 if end_pos is not None:
446 452 end_pos += 1
447 453
448 454 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
449 455 self.revisions[start_pos:end_pos]
450 456
451 457 for id_ in slice_:
452 458 cs = self.get_changeset(id_)
453 459 if branch_name and cs.branch != branch_name:
454 460 continue
455 461 if start_date and cs.date < start_date:
456 462 continue
457 463 if end_date and cs.date > end_date:
458 464 continue
459 465
460 466 yield cs
461 467
462 468 def pull(self, url):
463 469 """
464 470 Tries to pull changes from external location.
465 471 """
466 472 url = self._get_url(url)
467 473 try:
468 474 pull(self.baseui, self._repo, url)
469 475 except Abort, err:
470 476 # Propagate error but with vcs's type
471 477 raise RepositoryError(str(err))
472 478
473 479 @LazyProperty
474 480 def workdir(self):
475 481 """
476 482 Returns ``Workdir`` instance for this repository.
477 483 """
478 484 return MercurialWorkdir(self)
479 485
480 486 def get_config_value(self, section, name, config_file=None):
481 487 """
482 488 Returns configuration value for a given [``section``] and ``name``.
483 489
484 490 :param section: Section we want to retrieve value from
485 491 :param name: Name of configuration we want to retrieve
486 492 :param config_file: A path to file which should be used to retrieve
487 493 configuration from (might also be a list of file paths)
488 494 """
489 495 if config_file is None:
490 496 config_file = []
491 497 elif isinstance(config_file, basestring):
492 498 config_file = [config_file]
493 499
494 500 config = self._repo.ui
495 501 for path in config_file:
496 502 config.readconfig(path)
497 503 return config.config(section, name)
498 504
499 505 def get_user_name(self, config_file=None):
500 506 """
501 507 Returns user's name from global configuration file.
502 508
503 509 :param config_file: A path to file which should be used to retrieve
504 510 configuration from (might also be a list of file paths)
505 511 """
506 512 username = self.get_config_value('ui', 'username')
507 513 if username:
508 514 return author_name(username)
509 515 return None
510 516
511 517 def get_user_email(self, config_file=None):
512 518 """
513 519 Returns user's email from global configuration file.
514 520
515 521 :param config_file: A path to file which should be used to retrieve
516 522 configuration from (might also be a list of file paths)
517 523 """
518 524 username = self.get_config_value('ui', 'username')
519 525 if username:
520 526 return author_email(username)
521 527 return None
General Comments 0
You need to be logged in to leave comments. Login now