##// END OF EJS Templates
changelog pagination with branch filtering now uses...
marcink -
r3747:600ffde2 beta
parent child Browse files
Show More
@@ -1,125 +1,123
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changelog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changelog controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import request, url, session, tmpl_context as c
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 import rhodecode.lib.helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.helpers import RepoPage
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.graphmod import _colored, _dagwalker
39 39 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
40 40 from rhodecode.lib.utils2 import safe_int
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class ChangelogController(BaseRepoController):
46 46
47 47 @LoginRequired()
48 48 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
49 49 'repository.admin')
50 50 def __before__(self):
51 51 super(ChangelogController, self).__before__()
52 52 c.affected_files_cut_off = 60
53 53
54 54 def index(self):
55 55 limit = 100
56 56 default = 20
57 57 if request.params.get('size'):
58 58 try:
59 59 int_size = int(request.params.get('size'))
60 60 except ValueError:
61 61 int_size = default
62 62 c.size = max(min(int_size, limit), 1)
63 63 session['changelog_size'] = c.size
64 64 session.save()
65 65 else:
66 66 c.size = int(session.get('changelog_size', default))
67 67 # min size must be 1
68 68 c.size = max(c.size, 1)
69 69 p = safe_int(request.params.get('page', 1), 1)
70 70 branch_name = request.params.get('branch', None)
71 71 try:
72 if branch_name:
73 collection = [z for z in
74 c.rhodecode_repo.get_changesets(start=0,
75 branch_name=branch_name)]
72 collection = c.rhodecode_repo.get_changesets(start=0,
73 branch_name=branch_name)
76 74 c.total_cs = len(collection)
77 else:
78 collection = c.rhodecode_repo
79 c.total_cs = len(c.rhodecode_repo)
80 75
81 76 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
82 77 items_per_page=c.size, branch=branch_name)
83 78 collection = list(c.pagination)
84 page_revisions = [x.raw_id for x in collection]
79 page_revisions = [x.raw_id for x in c.pagination]
85 80 c.comments = c.rhodecode_db_repo.get_comments(page_revisions)
86 81 c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
87 82 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
88 83 log.error(traceback.format_exc())
89 84 h.flash(str(e), category='error')
90 85 return redirect(url('changelog_home', repo_name=c.repo_name))
91 86
92 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
93
94 87 c.branch_name = branch_name
95 88 c.branch_filters = [('', _('All Branches'))] + \
96 89 [(k, k) for k in c.rhodecode_repo.branches.keys()]
97 90
91 self._graph(c.rhodecode_repo, [x.revision for x in c.pagination],
92 c.total_cs, c.size, p)
93
98 94 return render('changelog/changelog.html')
99 95
100 96 def changelog_details(self, cs):
101 97 if request.environ.get('HTTP_X_PARTIAL_XHR'):
102 98 c.cs = c.rhodecode_repo.get_changeset(cs)
103 99 return render('changelog/changelog_details.html')
104 100
105 def _graph(self, repo, collection, repo_size, size, p):
101 def _graph(self, repo, revs_int, repo_size, size, p):
106 102 """
107 Generates a DAG graph for mercurial
103 Generates a DAG graph for repo
108 104
109 :param repo: repo instance
110 :param size: number of commits to show
111 :param p: page number
105 :param repo:
106 :param revs_int:
107 :param repo_size:
108 :param size:
109 :param p:
112 110 """
113 if not collection:
111 if not revs_int:
114 112 c.jsdata = json.dumps([])
115 113 return
116 114
117 115 data = []
118 revs = [x.revision for x in collection]
116 revs = revs_int
119 117
120 118 dag = _dagwalker(repo, revs, repo.alias)
121 119 dag = _colored(dag)
122 120 for (id, type, ctx, vtx, edges) in dag:
123 121 data.append(['', vtx, edges])
124 122
125 123 c.jsdata = json.dumps(data)
@@ -1,1004 +1,1028
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.base
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Base for all available scm backends
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import datetime
13 13 from itertools import chain
14 14 from rhodecode.lib.vcs.utils import author_name, author_email
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 17 from rhodecode.lib.vcs.conf import settings
18 18
19 19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 22 RepositoryError
23 23
24 24
25 25 class BaseRepository(object):
26 26 """
27 27 Base Repository for final backends
28 28
29 29 **Attributes**
30 30
31 31 ``DEFAULT_BRANCH_NAME``
32 32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 33
34 34 ``scm``
35 35 alias of scm, i.e. *git* or *hg*
36 36
37 37 ``repo``
38 38 object from external api
39 39
40 40 ``revisions``
41 41 list of all available revisions' ids, in ascending order
42 42
43 43 ``changesets``
44 44 storage dict caching returned changesets
45 45
46 46 ``path``
47 47 absolute path to the repository
48 48
49 49 ``branches``
50 50 branches as list of changesets
51 51
52 52 ``tags``
53 53 tags as list of changesets
54 54 """
55 55 scm = None
56 56 DEFAULT_BRANCH_NAME = None
57 57 EMPTY_CHANGESET = '0' * 40
58 58
59 59 def __init__(self, repo_path, create=False, **kwargs):
60 60 """
61 61 Initializes repository. Raises RepositoryError if repository could
62 62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 63 exists and ``create`` is set to True.
64 64
65 65 :param repo_path: local path of the repository
66 66 :param create=False: if set to True, would try to craete repository.
67 67 :param src_url=None: if set, should be proper url from which repository
68 68 would be cloned; requires ``create`` parameter to be set to True -
69 69 raises RepositoryError if src_url is set and create evaluates to
70 70 False
71 71 """
72 72 raise NotImplementedError
73 73
74 74 def __str__(self):
75 75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 76
77 77 def __repr__(self):
78 78 return self.__str__()
79 79
80 80 def __len__(self):
81 81 return self.count()
82 82
83 83 def __eq__(self, other):
84 84 same_instance = isinstance(other, self.__class__)
85 85 return same_instance and getattr(other, 'path', None) == self.path
86 86
87 87 def __ne__(self, other):
88 88 return not self.__eq__(other)
89 89
90 90 @LazyProperty
91 91 def alias(self):
92 92 for k, v in settings.BACKENDS.items():
93 93 if v.split('.')[-1] == str(self.__class__.__name__):
94 94 return k
95 95
96 96 @LazyProperty
97 97 def name(self):
98 98 raise NotImplementedError
99 99
100 100 @LazyProperty
101 101 def owner(self):
102 102 raise NotImplementedError
103 103
104 104 @LazyProperty
105 105 def description(self):
106 106 raise NotImplementedError
107 107
108 108 @LazyProperty
109 109 def size(self):
110 110 """
111 111 Returns combined size in bytes for all repository files
112 112 """
113 113
114 114 size = 0
115 115 try:
116 116 tip = self.get_changeset()
117 117 for topnode, dirs, files in tip.walk('/'):
118 118 for f in files:
119 119 size += tip.get_file_size(f.path)
120 120 for dir in dirs:
121 121 for f in files:
122 122 size += tip.get_file_size(f.path)
123 123
124 124 except RepositoryError, e:
125 125 pass
126 126 return size
127 127
128 128 def is_valid(self):
129 129 """
130 130 Validates repository.
131 131 """
132 132 raise NotImplementedError
133 133
134 134 def get_last_change(self):
135 135 self.get_changesets()
136 136
137 137 #==========================================================================
138 138 # CHANGESETS
139 139 #==========================================================================
140 140
141 141 def get_changeset(self, revision=None):
142 142 """
143 143 Returns instance of ``Changeset`` class. If ``revision`` is None, most
144 144 recent changeset is returned.
145 145
146 146 :raises ``EmptyRepositoryError``: if there are no revisions
147 147 """
148 148 raise NotImplementedError
149 149
150 150 def __iter__(self):
151 151 """
152 152 Allows Repository objects to be iterated.
153 153
154 154 *Requires* implementation of ``__getitem__`` method.
155 155 """
156 156 for revision in self.revisions:
157 157 yield self.get_changeset(revision)
158 158
159 159 def get_changesets(self, start=None, end=None, start_date=None,
160 160 end_date=None, branch_name=None, reverse=False):
161 161 """
162 162 Returns iterator of ``MercurialChangeset`` objects from start to end
163 163 not inclusive This should behave just like a list, ie. end is not
164 164 inclusive
165 165
166 166 :param start: None or str
167 167 :param end: None or str
168 168 :param start_date:
169 169 :param end_date:
170 170 :param branch_name:
171 171 :param reversed:
172 172 """
173 173 raise NotImplementedError
174 174
175 175 def __getslice__(self, i, j):
176 176 """
177 177 Returns a iterator of sliced repository
178 178 """
179 179 for rev in self.revisions[i:j]:
180 180 yield self.get_changeset(rev)
181 181
182 182 def __getitem__(self, key):
183 183 return self.get_changeset(key)
184 184
185 185 def count(self):
186 186 return len(self.revisions)
187 187
188 188 def tag(self, name, user, revision=None, message=None, date=None, **opts):
189 189 """
190 190 Creates and returns a tag for the given ``revision``.
191 191
192 192 :param name: name for new tag
193 193 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
194 194 :param revision: changeset id for which new tag would be created
195 195 :param message: message of the tag's commit
196 196 :param date: date of tag's commit
197 197
198 198 :raises TagAlreadyExistError: if tag with same name already exists
199 199 """
200 200 raise NotImplementedError
201 201
202 202 def remove_tag(self, name, user, message=None, date=None):
203 203 """
204 204 Removes tag with the given ``name``.
205 205
206 206 :param name: name of the tag to be removed
207 207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
208 208 :param message: message of the tag's removal commit
209 209 :param date: date of tag's removal commit
210 210
211 211 :raises TagDoesNotExistError: if tag with given name does not exists
212 212 """
213 213 raise NotImplementedError
214 214
215 215 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
216 216 context=3):
217 217 """
218 218 Returns (git like) *diff*, as plain text. Shows changes introduced by
219 219 ``rev2`` since ``rev1``.
220 220
221 221 :param rev1: Entry point from which diff is shown. Can be
222 222 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
223 223 the changes since empty state of the repository until ``rev2``
224 224 :param rev2: Until which revision changes should be shown.
225 225 :param ignore_whitespace: If set to ``True``, would not show whitespace
226 226 changes. Defaults to ``False``.
227 227 :param context: How many lines before/after changed lines should be
228 228 shown. Defaults to ``3``.
229 229 """
230 230 raise NotImplementedError
231 231
232 232 # ========== #
233 233 # COMMIT API #
234 234 # ========== #
235 235
236 236 @LazyProperty
237 237 def in_memory_changeset(self):
238 238 """
239 239 Returns ``InMemoryChangeset`` object for this repository.
240 240 """
241 241 raise NotImplementedError
242 242
243 243 def add(self, filenode, **kwargs):
244 244 """
245 245 Commit api function that will add given ``FileNode`` into this
246 246 repository.
247 247
248 248 :raises ``NodeAlreadyExistsError``: if there is a file with same path
249 249 already in repository
250 250 :raises ``NodeAlreadyAddedError``: if given node is already marked as
251 251 *added*
252 252 """
253 253 raise NotImplementedError
254 254
255 255 def remove(self, filenode, **kwargs):
256 256 """
257 257 Commit api function that will remove given ``FileNode`` into this
258 258 repository.
259 259
260 260 :raises ``EmptyRepositoryError``: if there are no changesets yet
261 261 :raises ``NodeDoesNotExistError``: if there is no file with given path
262 262 """
263 263 raise NotImplementedError
264 264
265 265 def commit(self, message, **kwargs):
266 266 """
267 267 Persists current changes made on this repository and returns newly
268 268 created changeset.
269 269
270 270 :raises ``NothingChangedError``: if no changes has been made
271 271 """
272 272 raise NotImplementedError
273 273
274 274 def get_state(self):
275 275 """
276 276 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
277 277 containing ``FileNode`` objects.
278 278 """
279 279 raise NotImplementedError
280 280
281 281 def get_config_value(self, section, name, config_file=None):
282 282 """
283 283 Returns configuration value for a given [``section``] and ``name``.
284 284
285 285 :param section: Section we want to retrieve value from
286 286 :param name: Name of configuration we want to retrieve
287 287 :param config_file: A path to file which should be used to retrieve
288 288 configuration from (might also be a list of file paths)
289 289 """
290 290 raise NotImplementedError
291 291
292 292 def get_user_name(self, config_file=None):
293 293 """
294 294 Returns user's name from global configuration file.
295 295
296 296 :param config_file: A path to file which should be used to retrieve
297 297 configuration from (might also be a list of file paths)
298 298 """
299 299 raise NotImplementedError
300 300
301 301 def get_user_email(self, config_file=None):
302 302 """
303 303 Returns user's email from global configuration file.
304 304
305 305 :param config_file: A path to file which should be used to retrieve
306 306 configuration from (might also be a list of file paths)
307 307 """
308 308 raise NotImplementedError
309 309
310 310 # =========== #
311 311 # WORKDIR API #
312 312 # =========== #
313 313
314 314 @LazyProperty
315 315 def workdir(self):
316 316 """
317 317 Returns ``Workdir`` instance for this repository.
318 318 """
319 319 raise NotImplementedError
320 320
321 321
322 322 class BaseChangeset(object):
323 323 """
324 324 Each backend should implement it's changeset representation.
325 325
326 326 **Attributes**
327 327
328 328 ``repository``
329 329 repository object within which changeset exists
330 330
331 331 ``id``
332 332 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
333 333
334 334 ``raw_id``
335 335 raw changeset representation (i.e. full 40 length sha for git
336 336 backend)
337 337
338 338 ``short_id``
339 339 shortened (if apply) version of ``raw_id``; it would be simple
340 340 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
341 341 as ``raw_id`` for subversion
342 342
343 343 ``revision``
344 344 revision number as integer
345 345
346 346 ``files``
347 347 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
348 348
349 349 ``dirs``
350 350 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
351 351
352 352 ``nodes``
353 353 combined list of ``Node`` objects
354 354
355 355 ``author``
356 356 author of the changeset, as unicode
357 357
358 358 ``message``
359 359 message of the changeset, as unicode
360 360
361 361 ``parents``
362 362 list of parent changesets
363 363
364 364 ``last``
365 365 ``True`` if this is last changeset in repository, ``False``
366 366 otherwise; trying to access this attribute while there is no
367 367 changesets would raise ``EmptyRepositoryError``
368 368 """
369 369 def __str__(self):
370 370 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
371 371 self.short_id)
372 372
373 373 def __repr__(self):
374 374 return self.__str__()
375 375
376 376 def __unicode__(self):
377 377 return u'%s:%s' % (self.revision, self.short_id)
378 378
379 379 def __eq__(self, other):
380 380 return self.raw_id == other.raw_id
381 381
382 382 def __json__(self):
383 383 return dict(
384 384 short_id=self.short_id,
385 385 raw_id=self.raw_id,
386 386 revision=self.revision,
387 387 message=self.message,
388 388 date=self.date,
389 389 author=self.author,
390 390 )
391 391
392 392 @LazyProperty
393 393 def last(self):
394 394 if self.repository is None:
395 395 raise ChangesetError("Cannot check if it's most recent revision")
396 396 return self.raw_id == self.repository.revisions[-1]
397 397
398 398 @LazyProperty
399 399 def parents(self):
400 400 """
401 401 Returns list of parents changesets.
402 402 """
403 403 raise NotImplementedError
404 404
405 405 @LazyProperty
406 406 def children(self):
407 407 """
408 408 Returns list of children changesets.
409 409 """
410 410 raise NotImplementedError
411 411
412 412 @LazyProperty
413 413 def id(self):
414 414 """
415 415 Returns string identifying this changeset.
416 416 """
417 417 raise NotImplementedError
418 418
419 419 @LazyProperty
420 420 def raw_id(self):
421 421 """
422 422 Returns raw string identifying this changeset.
423 423 """
424 424 raise NotImplementedError
425 425
426 426 @LazyProperty
427 427 def short_id(self):
428 428 """
429 429 Returns shortened version of ``raw_id`` attribute, as string,
430 430 identifying this changeset, useful for web representation.
431 431 """
432 432 raise NotImplementedError
433 433
434 434 @LazyProperty
435 435 def revision(self):
436 436 """
437 437 Returns integer identifying this changeset.
438 438
439 439 """
440 440 raise NotImplementedError
441 441
442 442 @LazyProperty
443 443 def committer(self):
444 444 """
445 445 Returns Committer for given commit
446 446 """
447 447
448 448 raise NotImplementedError
449 449
450 450 @LazyProperty
451 451 def committer_name(self):
452 452 """
453 453 Returns Author name for given commit
454 454 """
455 455
456 456 return author_name(self.committer)
457 457
458 458 @LazyProperty
459 459 def committer_email(self):
460 460 """
461 461 Returns Author email address for given commit
462 462 """
463 463
464 464 return author_email(self.committer)
465 465
466 466 @LazyProperty
467 467 def author(self):
468 468 """
469 469 Returns Author for given commit
470 470 """
471 471
472 472 raise NotImplementedError
473 473
474 474 @LazyProperty
475 475 def author_name(self):
476 476 """
477 477 Returns Author name for given commit
478 478 """
479 479
480 480 return author_name(self.author)
481 481
482 482 @LazyProperty
483 483 def author_email(self):
484 484 """
485 485 Returns Author email address for given commit
486 486 """
487 487
488 488 return author_email(self.author)
489 489
490 490 def get_file_mode(self, path):
491 491 """
492 492 Returns stat mode of the file at the given ``path``.
493 493 """
494 494 raise NotImplementedError
495 495
496 496 def get_file_content(self, path):
497 497 """
498 498 Returns content of the file at the given ``path``.
499 499 """
500 500 raise NotImplementedError
501 501
502 502 def get_file_size(self, path):
503 503 """
504 504 Returns size of the file at the given ``path``.
505 505 """
506 506 raise NotImplementedError
507 507
508 508 def get_file_changeset(self, path):
509 509 """
510 510 Returns last commit of the file at the given ``path``.
511 511 """
512 512 raise NotImplementedError
513 513
514 514 def get_file_history(self, path):
515 515 """
516 516 Returns history of file as reversed list of ``Changeset`` objects for
517 517 which file at given ``path`` has been modified.
518 518 """
519 519 raise NotImplementedError
520 520
521 521 def get_nodes(self, path):
522 522 """
523 523 Returns combined ``DirNode`` and ``FileNode`` objects list representing
524 524 state of changeset at the given ``path``.
525 525
526 526 :raises ``ChangesetError``: if node at the given ``path`` is not
527 527 instance of ``DirNode``
528 528 """
529 529 raise NotImplementedError
530 530
531 531 def get_node(self, path):
532 532 """
533 533 Returns ``Node`` object from the given ``path``.
534 534
535 535 :raises ``NodeDoesNotExistError``: if there is no node at the given
536 536 ``path``
537 537 """
538 538 raise NotImplementedError
539 539
540 540 def fill_archive(self, stream=None, kind='tgz', prefix=None):
541 541 """
542 542 Fills up given stream.
543 543
544 544 :param stream: file like object.
545 545 :param kind: one of following: ``zip``, ``tar``, ``tgz``
546 546 or ``tbz2``. Default: ``tgz``.
547 547 :param prefix: name of root directory in archive.
548 548 Default is repository name and changeset's raw_id joined with dash.
549 549
550 550 repo-tip.<kind>
551 551 """
552 552
553 553 raise NotImplementedError
554 554
555 555 def get_chunked_archive(self, **kwargs):
556 556 """
557 557 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
558 558
559 559 :param chunk_size: extra parameter which controls size of returned
560 560 chunks. Default:8k.
561 561 """
562 562
563 563 chunk_size = kwargs.pop('chunk_size', 8192)
564 564 stream = kwargs.get('stream')
565 565 self.fill_archive(**kwargs)
566 566 while True:
567 567 data = stream.read(chunk_size)
568 568 if not data:
569 569 break
570 570 yield data
571 571
572 572 @LazyProperty
573 573 def root(self):
574 574 """
575 575 Returns ``RootNode`` object for this changeset.
576 576 """
577 577 return self.get_node('')
578 578
579 579 def next(self, branch=None):
580 580 """
581 581 Returns next changeset from current, if branch is gives it will return
582 582 next changeset belonging to this branch
583 583
584 584 :param branch: show changesets within the given named branch
585 585 """
586 586 raise NotImplementedError
587 587
588 588 def prev(self, branch=None):
589 589 """
590 590 Returns previous changeset from current, if branch is gives it will
591 591 return previous changeset belonging to this branch
592 592
593 593 :param branch: show changesets within the given named branch
594 594 """
595 595 raise NotImplementedError
596 596
597 597 @LazyProperty
598 598 def added(self):
599 599 """
600 600 Returns list of added ``FileNode`` objects.
601 601 """
602 602 raise NotImplementedError
603 603
604 604 @LazyProperty
605 605 def changed(self):
606 606 """
607 607 Returns list of modified ``FileNode`` objects.
608 608 """
609 609 raise NotImplementedError
610 610
611 611 @LazyProperty
612 612 def removed(self):
613 613 """
614 614 Returns list of removed ``FileNode`` objects.
615 615 """
616 616 raise NotImplementedError
617 617
618 618 @LazyProperty
619 619 def size(self):
620 620 """
621 621 Returns total number of bytes from contents of all filenodes.
622 622 """
623 623 return sum((node.size for node in self.get_filenodes_generator()))
624 624
625 625 def walk(self, topurl=''):
626 626 """
627 627 Similar to os.walk method. Insted of filesystem it walks through
628 628 changeset starting at given ``topurl``. Returns generator of tuples
629 629 (topnode, dirnodes, filenodes).
630 630 """
631 631 topnode = self.get_node(topurl)
632 632 yield (topnode, topnode.dirs, topnode.files)
633 633 for dirnode in topnode.dirs:
634 634 for tup in self.walk(dirnode.path):
635 635 yield tup
636 636
637 637 def get_filenodes_generator(self):
638 638 """
639 639 Returns generator that yields *all* file nodes.
640 640 """
641 641 for topnode, dirs, files in self.walk():
642 642 for node in files:
643 643 yield node
644 644
645 645 def as_dict(self):
646 646 """
647 647 Returns dictionary with changeset's attributes and their values.
648 648 """
649 649 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
650 650 'revision', 'date', 'message'])
651 651 data['author'] = {'name': self.author_name, 'email': self.author_email}
652 652 data['added'] = [node.path for node in self.added]
653 653 data['changed'] = [node.path for node in self.changed]
654 654 data['removed'] = [node.path for node in self.removed]
655 655 return data
656 656
657 657
658 658 class BaseWorkdir(object):
659 659 """
660 660 Working directory representation of single repository.
661 661
662 662 :attribute: repository: repository object of working directory
663 663 """
664 664
665 665 def __init__(self, repository):
666 666 self.repository = repository
667 667
668 668 def get_branch(self):
669 669 """
670 670 Returns name of current branch.
671 671 """
672 672 raise NotImplementedError
673 673
674 674 def get_changeset(self):
675 675 """
676 676 Returns current changeset.
677 677 """
678 678 raise NotImplementedError
679 679
680 680 def get_added(self):
681 681 """
682 682 Returns list of ``FileNode`` objects marked as *new* in working
683 683 directory.
684 684 """
685 685 raise NotImplementedError
686 686
687 687 def get_changed(self):
688 688 """
689 689 Returns list of ``FileNode`` objects *changed* in working directory.
690 690 """
691 691 raise NotImplementedError
692 692
693 693 def get_removed(self):
694 694 """
695 695 Returns list of ``RemovedFileNode`` objects marked as *removed* in
696 696 working directory.
697 697 """
698 698 raise NotImplementedError
699 699
700 700 def get_untracked(self):
701 701 """
702 702 Returns list of ``FileNode`` objects which are present within working
703 703 directory however are not tracked by repository.
704 704 """
705 705 raise NotImplementedError
706 706
707 707 def get_status(self):
708 708 """
709 709 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
710 710 lists.
711 711 """
712 712 raise NotImplementedError
713 713
714 714 def commit(self, message, **kwargs):
715 715 """
716 716 Commits local (from working directory) changes and returns newly
717 717 created
718 718 ``Changeset``. Updates repository's ``revisions`` list.
719 719
720 720 :raises ``CommitError``: if any error occurs while committing
721 721 """
722 722 raise NotImplementedError
723 723
724 724 def update(self, revision=None):
725 725 """
726 726 Fetches content of the given revision and populates it within working
727 727 directory.
728 728 """
729 729 raise NotImplementedError
730 730
731 731 def checkout_branch(self, branch=None):
732 732 """
733 733 Checks out ``branch`` or the backend's default branch.
734 734
735 735 Raises ``BranchDoesNotExistError`` if the branch does not exist.
736 736 """
737 737 raise NotImplementedError
738 738
739 739
740 740 class BaseInMemoryChangeset(object):
741 741 """
742 742 Represents differences between repository's state (most recent head) and
743 743 changes made *in place*.
744 744
745 745 **Attributes**
746 746
747 747 ``repository``
748 748 repository object for this in-memory-changeset
749 749
750 750 ``added``
751 751 list of ``FileNode`` objects marked as *added*
752 752
753 753 ``changed``
754 754 list of ``FileNode`` objects marked as *changed*
755 755
756 756 ``removed``
757 757 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
758 758 *removed*
759 759
760 760 ``parents``
761 761 list of ``Changeset`` representing parents of in-memory changeset.
762 762 Should always be 2-element sequence.
763 763
764 764 """
765 765
766 766 def __init__(self, repository):
767 767 self.repository = repository
768 768 self.added = []
769 769 self.changed = []
770 770 self.removed = []
771 771 self.parents = []
772 772
773 773 def add(self, *filenodes):
774 774 """
775 775 Marks given ``FileNode`` objects as *to be committed*.
776 776
777 777 :raises ``NodeAlreadyExistsError``: if node with same path exists at
778 778 latest changeset
779 779 :raises ``NodeAlreadyAddedError``: if node with same path is already
780 780 marked as *added*
781 781 """
782 782 # Check if not already marked as *added* first
783 783 for node in filenodes:
784 784 if node.path in (n.path for n in self.added):
785 785 raise NodeAlreadyAddedError("Such FileNode %s is already "
786 786 "marked for addition" % node.path)
787 787 for node in filenodes:
788 788 self.added.append(node)
789 789
790 790 def change(self, *filenodes):
791 791 """
792 792 Marks given ``FileNode`` objects to be *changed* in next commit.
793 793
794 794 :raises ``EmptyRepositoryError``: if there are no changesets yet
795 795 :raises ``NodeAlreadyExistsError``: if node with same path is already
796 796 marked to be *changed*
797 797 :raises ``NodeAlreadyRemovedError``: if node with same path is already
798 798 marked to be *removed*
799 799 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
800 800 changeset
801 801 :raises ``NodeNotChangedError``: if node hasn't really be changed
802 802 """
803 803 for node in filenodes:
804 804 if node.path in (n.path for n in self.removed):
805 805 raise NodeAlreadyRemovedError("Node at %s is already marked "
806 806 "as removed" % node.path)
807 807 try:
808 808 self.repository.get_changeset()
809 809 except EmptyRepositoryError:
810 810 raise EmptyRepositoryError("Nothing to change - try to *add* new "
811 811 "nodes rather than changing them")
812 812 for node in filenodes:
813 813 if node.path in (n.path for n in self.changed):
814 814 raise NodeAlreadyChangedError("Node at '%s' is already "
815 815 "marked as changed" % node.path)
816 816 self.changed.append(node)
817 817
818 818 def remove(self, *filenodes):
819 819 """
820 820 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
821 821 *removed* in next commit.
822 822
823 823 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
824 824 be *removed*
825 825 :raises ``NodeAlreadyChangedError``: if node has been already marked to
826 826 be *changed*
827 827 """
828 828 for node in filenodes:
829 829 if node.path in (n.path for n in self.removed):
830 830 raise NodeAlreadyRemovedError("Node is already marked to "
831 831 "for removal at %s" % node.path)
832 832 if node.path in (n.path for n in self.changed):
833 833 raise NodeAlreadyChangedError("Node is already marked to "
834 834 "be changed at %s" % node.path)
835 835 # We only mark node as *removed* - real removal is done by
836 836 # commit method
837 837 self.removed.append(node)
838 838
839 839 def reset(self):
840 840 """
841 841 Resets this instance to initial state (cleans ``added``, ``changed``
842 842 and ``removed`` lists).
843 843 """
844 844 self.added = []
845 845 self.changed = []
846 846 self.removed = []
847 847 self.parents = []
848 848
849 849 def get_ipaths(self):
850 850 """
851 851 Returns generator of paths from nodes marked as added, changed or
852 852 removed.
853 853 """
854 854 for node in chain(self.added, self.changed, self.removed):
855 855 yield node.path
856 856
857 857 def get_paths(self):
858 858 """
859 859 Returns list of paths from nodes marked as added, changed or removed.
860 860 """
861 861 return list(self.get_ipaths())
862 862
863 863 def check_integrity(self, parents=None):
864 864 """
865 865 Checks in-memory changeset's integrity. Also, sets parents if not
866 866 already set.
867 867
868 868 :raises CommitError: if any error occurs (i.e.
869 869 ``NodeDoesNotExistError``).
870 870 """
871 871 if not self.parents:
872 872 parents = parents or []
873 873 if len(parents) == 0:
874 874 try:
875 875 parents = [self.repository.get_changeset(), None]
876 876 except EmptyRepositoryError:
877 877 parents = [None, None]
878 878 elif len(parents) == 1:
879 879 parents += [None]
880 880 self.parents = parents
881 881
882 882 # Local parents, only if not None
883 883 parents = [p for p in self.parents if p]
884 884
885 885 # Check nodes marked as added
886 886 for p in parents:
887 887 for node in self.added:
888 888 try:
889 889 p.get_node(node.path)
890 890 except NodeDoesNotExistError:
891 891 pass
892 892 else:
893 893 raise NodeAlreadyExistsError("Node at %s already exists "
894 894 "at %s" % (node.path, p))
895 895
896 896 # Check nodes marked as changed
897 897 missing = set(self.changed)
898 898 not_changed = set(self.changed)
899 899 if self.changed and not parents:
900 900 raise NodeDoesNotExistError(str(self.changed[0].path))
901 901 for p in parents:
902 902 for node in self.changed:
903 903 try:
904 904 old = p.get_node(node.path)
905 905 missing.remove(node)
906 906 if old.content != node.content:
907 907 not_changed.remove(node)
908 908 except NodeDoesNotExistError:
909 909 pass
910 910 if self.changed and missing:
911 911 raise NodeDoesNotExistError("Node at %s is missing "
912 912 "(parents: %s)" % (node.path, parents))
913 913
914 914 if self.changed and not_changed:
915 915 raise NodeNotChangedError("Node at %s wasn't actually changed "
916 916 "since parents' changesets: %s" % (not_changed.pop().path,
917 917 parents)
918 918 )
919 919
920 920 # Check nodes marked as removed
921 921 if self.removed and not parents:
922 922 raise NodeDoesNotExistError("Cannot remove node at %s as there "
923 923 "were no parents specified" % self.removed[0].path)
924 924 really_removed = set()
925 925 for p in parents:
926 926 for node in self.removed:
927 927 try:
928 928 p.get_node(node.path)
929 929 really_removed.add(node)
930 930 except ChangesetError:
931 931 pass
932 932 not_removed = set(self.removed) - really_removed
933 933 if not_removed:
934 934 raise NodeDoesNotExistError("Cannot remove node at %s from "
935 935 "following parents: %s" % (not_removed[0], parents))
936 936
937 937 def commit(self, message, author, parents=None, branch=None, date=None,
938 938 **kwargs):
939 939 """
940 940 Performs in-memory commit (doesn't check workdir in any way) and
941 941 returns newly created ``Changeset``. Updates repository's
942 942 ``revisions``.
943 943
944 944 .. note::
945 945 While overriding this method each backend's should call
946 946 ``self.check_integrity(parents)`` in the first place.
947 947
948 948 :param message: message of the commit
949 949 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
950 950 :param parents: single parent or sequence of parents from which commit
951 951 would be derieved
952 952 :param date: ``datetime.datetime`` instance. Defaults to
953 953 ``datetime.datetime.now()``.
954 954 :param branch: branch name, as string. If none given, default backend's
955 955 branch would be used.
956 956
957 957 :raises ``CommitError``: if any error occurs while committing
958 958 """
959 959 raise NotImplementedError
960 960
961 961
962 962 class EmptyChangeset(BaseChangeset):
963 963 """
964 964 An dummy empty changeset. It's possible to pass hash when creating
965 965 an EmptyChangeset
966 966 """
967 967
968 968 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
969 969 alias=None, revision=-1, message='', author='', date=None):
970 970 self._empty_cs = cs
971 971 self.revision = revision
972 972 self.message = message
973 973 self.author = author
974 974 self.date = date or datetime.datetime.fromtimestamp(0)
975 975 self.repository = repo
976 976 self.requested_revision = requested_revision
977 977 self.alias = alias
978 978
979 979 @LazyProperty
980 980 def raw_id(self):
981 981 """
982 982 Returns raw string identifying this changeset, useful for web
983 983 representation.
984 984 """
985 985
986 986 return self._empty_cs
987 987
988 988 @LazyProperty
989 989 def branch(self):
990 990 from rhodecode.lib.vcs.backends import get_backend
991 991 return get_backend(self.alias).DEFAULT_BRANCH_NAME
992 992
993 993 @LazyProperty
994 994 def short_id(self):
995 995 return self.raw_id[:12]
996 996
997 997 def get_file_changeset(self, path):
998 998 return self
999 999
1000 1000 def get_file_content(self, path):
1001 1001 return u''
1002 1002
1003 1003 def get_file_size(self, path):
1004 1004 return 0
1005
1006
1007 class CollectionGenerator(object):
1008
1009 def __init__(self, repo, revs):
1010 self.repo = repo
1011 self.revs = revs
1012
1013 def __len__(self):
1014 return len(self.revs)
1015
1016 def __iter__(self):
1017 for rev in self.revs:
1018 yield self.repo.get_changeset(rev)
1019
1020 def __getslice__(self, i, j):
1021 """
1022 Returns a iterator of sliced repository
1023 """
1024 sliced_revs = self.revs[i:j]
1025 return CollectionGenerator(self.repo, sliced_revs)
1026
1027 def __repr__(self):
1028 return 'CollectionGenerator<%s>' % (len(self))
@@ -1,693 +1,692
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 import logging
17 17 import traceback
18 18 import urllib
19 19 import urllib2
20 20 from dulwich.repo import Repo, NotGitRepository
21 21 from dulwich.objects import Tag
22 22 from string import Template
23 23
24 24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
26 26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 35 from rhodecode.lib.vcs.utils.paths import abspath
36 36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 37 from .workdir import GitWorkdir
38 38 from .changeset import GitChangeset
39 39 from .inmemory import GitInMemoryChangeset
40 40 from .config import ConfigFile
41 41 from rhodecode.lib import subprocessio
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class GitRepository(BaseRepository):
48 48 """
49 49 Git repository backend.
50 50 """
51 51 DEFAULT_BRANCH_NAME = 'master'
52 52 scm = 'git'
53 53
54 54 def __init__(self, repo_path, create=False, src_url=None,
55 55 update_after_clone=False, bare=False):
56 56
57 57 self.path = abspath(repo_path)
58 58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 59 self.bare = repo.bare
60 60
61 61 self._config_files = [
62 62 bare and abspath(self.path, 'config')
63 63 or abspath(self.path, '.git', 'config'),
64 64 abspath(get_user_home(), '.gitconfig'),
65 65 ]
66 66
67 67 @property
68 68 def _repo(self):
69 69 return Repo(self.path)
70 70
71 71 @property
72 72 def head(self):
73 73 try:
74 74 return self._repo.head()
75 75 except KeyError:
76 76 return None
77 77
78 78 @LazyProperty
79 79 def revisions(self):
80 80 """
81 81 Returns list of revisions' ids, in ascending order. Being lazy
82 82 attribute allows external tools to inject shas from cache.
83 83 """
84 84 return self._get_all_revisions()
85 85
86 86 @classmethod
87 87 def _run_git_command(cls, cmd, **opts):
88 88 """
89 89 Runs given ``cmd`` as git command and returns tuple
90 90 (stdout, stderr).
91 91
92 92 :param cmd: git command to be executed
93 93 :param opts: env options to pass into Subprocess command
94 94 """
95 95
96 96 if '_bare' in opts:
97 97 _copts = []
98 98 del opts['_bare']
99 99 else:
100 100 _copts = ['-c', 'core.quotepath=false', ]
101 101 safe_call = False
102 102 if '_safe' in opts:
103 103 #no exc on failure
104 104 del opts['_safe']
105 105 safe_call = True
106 106
107 107 _str_cmd = False
108 108 if isinstance(cmd, basestring):
109 109 cmd = [cmd]
110 110 _str_cmd = True
111 111
112 112 gitenv = os.environ
113 113 # need to clean fix GIT_DIR !
114 114 if 'GIT_DIR' in gitenv:
115 115 del gitenv['GIT_DIR']
116 116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
117 117
118 118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
119 119 cmd = [_git_path] + _copts + cmd
120 120 if _str_cmd:
121 121 cmd = ' '.join(cmd)
122 122 try:
123 123 _opts = dict(
124 124 env=gitenv,
125 125 shell=False,
126 126 )
127 127 _opts.update(opts)
128 128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
129 129 except (EnvironmentError, OSError), err:
130 130 tb_err = ("Couldn't run git command (%s).\n"
131 131 "Original error was:%s\n" % (cmd, err))
132 132 log.error(tb_err)
133 133 if safe_call:
134 134 return '', err
135 135 else:
136 136 raise RepositoryError(tb_err)
137 137
138 138 return ''.join(p.output), ''.join(p.error)
139 139
140 140 def run_git_command(self, cmd):
141 141 opts = {}
142 142 if os.path.isdir(self.path):
143 143 opts['cwd'] = self.path
144 144 return self._run_git_command(cmd, **opts)
145 145
146 146 @classmethod
147 147 def _check_url(cls, url):
148 148 """
149 149 Functon will check given url and try to verify if it's a valid
150 150 link. Sometimes it may happened that mercurial will issue basic
151 151 auth request that can cause whole API to hang when used from python
152 152 or other external calls.
153 153
154 154 On failures it'll raise urllib2.HTTPError
155 155 """
156 156 from mercurial.util import url as Url
157 157
158 158 # those authnadlers are patched for python 2.6.5 bug an
159 159 # infinit looping when given invalid resources
160 160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
161 161
162 162 # check first if it's not an local url
163 163 if os.path.isdir(url) or url.startswith('file:'):
164 164 return True
165 165
166 166 if('+' in url[:url.find('://')]):
167 167 url = url[url.find('+') + 1:]
168 168
169 169 handlers = []
170 170 test_uri, authinfo = Url(url).authinfo()
171 171 if not test_uri.endswith('info/refs'):
172 172 test_uri = test_uri.rstrip('/') + '/info/refs'
173 173 if authinfo:
174 174 #create a password manager
175 175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
176 176 passmgr.add_password(*authinfo)
177 177
178 178 handlers.extend((httpbasicauthhandler(passmgr),
179 179 httpdigestauthhandler(passmgr)))
180 180
181 181 o = urllib2.build_opener(*handlers)
182 182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
183 183
184 184 q = {"service": 'git-upload-pack'}
185 185 qs = '?%s' % urllib.urlencode(q)
186 186 cu = "%s%s" % (test_uri, qs)
187 187 req = urllib2.Request(cu, None, {})
188 188
189 189 try:
190 190 resp = o.open(req)
191 191 return resp.code == 200
192 192 except Exception, e:
193 193 # means it cannot be cloned
194 194 raise urllib2.URLError("[%s] %s" % (url, e))
195 195
196 196 def _get_repo(self, create, src_url=None, update_after_clone=False,
197 197 bare=False):
198 198 if create and os.path.exists(self.path):
199 199 raise RepositoryError("Location already exist")
200 200 if src_url and not create:
201 201 raise RepositoryError("Create should be set to True if src_url is "
202 202 "given (clone operation creates repository)")
203 203 try:
204 204 if create and src_url:
205 205 GitRepository._check_url(src_url)
206 206 self.clone(src_url, update_after_clone, bare)
207 207 return Repo(self.path)
208 208 elif create:
209 209 os.mkdir(self.path)
210 210 if bare:
211 211 return Repo.init_bare(self.path)
212 212 else:
213 213 return Repo.init(self.path)
214 214 else:
215 215 return self._repo
216 216 except (NotGitRepository, OSError), err:
217 217 raise RepositoryError(err)
218 218
219 219 def _get_all_revisions(self):
220 220 # we must check if this repo is not empty, since later command
221 221 # fails if it is. And it's cheaper to ask than throw the subprocess
222 222 # errors
223 223 try:
224 224 self._repo.head()
225 225 except KeyError:
226 226 return []
227 227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
228 228 '--all').strip()
229 229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
230 230 try:
231 231 so, se = self.run_git_command(cmd)
232 232 except RepositoryError:
233 233 # Can be raised for empty repositories
234 234 return []
235 235 return so.splitlines()
236 236
237 237 def _get_all_revisions2(self):
238 238 #alternate implementation using dulwich
239 239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
240 240 if x[1][1] != 'T']
241 241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
242 242
243 243 def _get_revision(self, revision):
244 244 """
245 245 For git backend we always return integer here. This way we ensure
246 246 that changset's revision attribute would become integer.
247 247 """
248 248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
249 249 is_bstr = lambda o: isinstance(o, (str, unicode))
250 250 is_null = lambda o: len(o) == revision.count('0')
251 251
252 252 if len(self.revisions) == 0:
253 253 raise EmptyRepositoryError("There are no changesets yet")
254 254
255 255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
256 256 revision = self.revisions[-1]
257 257
258 258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
259 259 or isinstance(revision, int) or is_null(revision)):
260 260 try:
261 261 revision = self.revisions[int(revision)]
262 262 except Exception:
263 263 raise ChangesetDoesNotExistError("Revision %s does not exist "
264 264 "for this repository" % (revision))
265 265
266 266 elif is_bstr(revision):
267 267 # get by branch/tag name
268 268 _ref_revision = self._parsed_refs.get(revision)
269 269 _tags_shas = self.tags.values()
270 270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
271 271 return _ref_revision[0]
272 272
273 273 # maybe it's a tag ? we don't have them in self.revisions
274 274 elif revision in _tags_shas:
275 275 return _tags_shas[_tags_shas.index(revision)]
276 276
277 277 elif not pattern.match(revision) or revision not in self.revisions:
278 278 raise ChangesetDoesNotExistError("Revision %s does not exist "
279 279 "for this repository" % (revision))
280 280
281 281 # Ensure we return full id
282 282 if not pattern.match(str(revision)):
283 283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
284 284 % revision)
285 285 return revision
286 286
287 287 def _get_archives(self, archive_name='tip'):
288 288
289 289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
290 290 yield {"type": i[0], "extension": i[1], "node": archive_name}
291 291
292 292 def _get_url(self, url):
293 293 """
294 294 Returns normalized url. If schema is not given, would fall to
295 295 filesystem (``file:///``) schema.
296 296 """
297 297 url = str(url)
298 298 if url != 'default' and not '://' in url:
299 299 url = ':///'.join(('file', url))
300 300 return url
301 301
302 302 def get_hook_location(self):
303 303 """
304 304 returns absolute path to location where hooks are stored
305 305 """
306 306 loc = os.path.join(self.path, 'hooks')
307 307 if not self.bare:
308 308 loc = os.path.join(self.path, '.git', 'hooks')
309 309 return loc
310 310
311 311 @LazyProperty
312 312 def name(self):
313 313 return os.path.basename(self.path)
314 314
315 315 @LazyProperty
316 316 def last_change(self):
317 317 """
318 318 Returns last change made on this repository as datetime object
319 319 """
320 320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
321 321
322 322 def _get_mtime(self):
323 323 try:
324 324 return time.mktime(self.get_changeset().date.timetuple())
325 325 except RepositoryError:
326 326 idx_loc = '' if self.bare else '.git'
327 327 # fallback to filesystem
328 328 in_path = os.path.join(self.path, idx_loc, "index")
329 329 he_path = os.path.join(self.path, idx_loc, "HEAD")
330 330 if os.path.exists(in_path):
331 331 return os.stat(in_path).st_mtime
332 332 else:
333 333 return os.stat(he_path).st_mtime
334 334
335 335 @LazyProperty
336 336 def description(self):
337 337 idx_loc = '' if self.bare else '.git'
338 338 undefined_description = u'unknown'
339 339 description_path = os.path.join(self.path, idx_loc, 'description')
340 340 if os.path.isfile(description_path):
341 341 return safe_unicode(open(description_path).read())
342 342 else:
343 343 return undefined_description
344 344
345 345 @LazyProperty
346 346 def contact(self):
347 347 undefined_contact = u'Unknown'
348 348 return undefined_contact
349 349
350 350 @property
351 351 def branches(self):
352 352 if not self.revisions:
353 353 return {}
354 354 sortkey = lambda ctx: ctx[0]
355 355 _branches = [(x[0], x[1][0])
356 356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
357 357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
358 358
359 359 @LazyProperty
360 360 def tags(self):
361 361 return self._get_tags()
362 362
363 363 def _get_tags(self):
364 364 if not self.revisions:
365 365 return {}
366 366
367 367 sortkey = lambda ctx: ctx[0]
368 368 _tags = [(x[0], x[1][0])
369 369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
370 370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
371 371
372 372 def tag(self, name, user, revision=None, message=None, date=None,
373 373 **kwargs):
374 374 """
375 375 Creates and returns a tag for the given ``revision``.
376 376
377 377 :param name: name for new tag
378 378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
379 379 :param revision: changeset id for which new tag would be created
380 380 :param message: message of the tag's commit
381 381 :param date: date of tag's commit
382 382
383 383 :raises TagAlreadyExistError: if tag with same name already exists
384 384 """
385 385 if name in self.tags:
386 386 raise TagAlreadyExistError("Tag %s already exists" % name)
387 387 changeset = self.get_changeset(revision)
388 388 message = message or "Added tag %s for commit %s" % (name,
389 389 changeset.raw_id)
390 390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
391 391
392 392 self._parsed_refs = self._get_parsed_refs()
393 393 self.tags = self._get_tags()
394 394 return changeset
395 395
396 396 def remove_tag(self, name, user, message=None, date=None):
397 397 """
398 398 Removes tag with the given ``name``.
399 399
400 400 :param name: name of the tag to be removed
401 401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
402 402 :param message: message of the tag's removal commit
403 403 :param date: date of tag's removal commit
404 404
405 405 :raises TagDoesNotExistError: if tag with given name does not exists
406 406 """
407 407 if name not in self.tags:
408 408 raise TagDoesNotExistError("Tag %s does not exist" % name)
409 409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
410 410 try:
411 411 os.remove(tagpath)
412 412 self._parsed_refs = self._get_parsed_refs()
413 413 self.tags = self._get_tags()
414 414 except OSError, e:
415 415 raise RepositoryError(e.strerror)
416 416
417 417 @LazyProperty
418 418 def _parsed_refs(self):
419 419 return self._get_parsed_refs()
420 420
421 421 def _get_parsed_refs(self):
422 422 # cache the property
423 423 _repo = self._repo
424 424 refs = _repo.get_refs()
425 425 keys = [('refs/heads/', 'H'),
426 426 ('refs/remotes/origin/', 'RH'),
427 427 ('refs/tags/', 'T')]
428 428 _refs = {}
429 429 for ref, sha in refs.iteritems():
430 430 for k, type_ in keys:
431 431 if ref.startswith(k):
432 432 _key = ref[len(k):]
433 433 if type_ == 'T':
434 434 obj = _repo.get_object(sha)
435 435 if isinstance(obj, Tag):
436 436 sha = _repo.get_object(sha).object[1]
437 437 _refs[_key] = [sha, type_]
438 438 break
439 439 return _refs
440 440
441 441 def _heads(self, reverse=False):
442 442 refs = self._repo.get_refs()
443 443 heads = {}
444 444
445 445 for key, val in refs.items():
446 446 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
447 447 if key.startswith(ref_key):
448 448 n = key[len(ref_key):]
449 449 if n not in ['HEAD']:
450 450 heads[n] = val
451 451
452 452 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
453 453
454 454 def get_changeset(self, revision=None):
455 455 """
456 456 Returns ``GitChangeset`` object representing commit from git repository
457 457 at the given revision or head (most recent commit) if None given.
458 458 """
459 459 if isinstance(revision, GitChangeset):
460 460 return revision
461 461 revision = self._get_revision(revision)
462 462 changeset = GitChangeset(repository=self, revision=revision)
463 463 return changeset
464 464
465 465 def get_changesets(self, start=None, end=None, start_date=None,
466 466 end_date=None, branch_name=None, reverse=False):
467 467 """
468 468 Returns iterator of ``GitChangeset`` objects from start to end (both
469 469 are inclusive), in ascending date order (unless ``reverse`` is set).
470 470
471 471 :param start: changeset ID, as str; first returned changeset
472 472 :param end: changeset ID, as str; last returned changeset
473 473 :param start_date: if specified, changesets with commit date less than
474 474 ``start_date`` would be filtered out from returned set
475 475 :param end_date: if specified, changesets with commit date greater than
476 476 ``end_date`` would be filtered out from returned set
477 477 :param branch_name: if specified, changesets not reachable from given
478 478 branch would be filtered out from returned set
479 479 :param reverse: if ``True``, returned generator would be reversed
480 480 (meaning that returned changesets would have descending date order)
481 481
482 482 :raise BranchDoesNotExistError: If given ``branch_name`` does not
483 483 exist.
484 484 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
485 485 ``end`` could not be found.
486 486
487 487 """
488 488 if branch_name and branch_name not in self.branches:
489 489 raise BranchDoesNotExistError("Branch '%s' not found" \
490 490 % branch_name)
491 491 # %H at format means (full) commit hash, initial hashes are retrieved
492 492 # in ascending date order
493 493 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
494 494 cmd_params = {}
495 495 if start_date:
496 496 cmd_template += ' --since "$since"'
497 497 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
498 498 if end_date:
499 499 cmd_template += ' --until "$until"'
500 500 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
501 501 if branch_name:
502 502 cmd_template += ' $branch_name'
503 503 cmd_params['branch_name'] = branch_name
504 504 else:
505 505 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
506 506 '--all').strip()
507 507 cmd_template += ' %s' % (rev_filter)
508 508
509 509 cmd = Template(cmd_template).safe_substitute(**cmd_params)
510 510 revs = self.run_git_command(cmd)[0].splitlines()
511 511 start_pos = 0
512 512 end_pos = len(revs)
513 513 if start:
514 514 _start = self._get_revision(start)
515 515 try:
516 516 start_pos = revs.index(_start)
517 517 except ValueError:
518 518 pass
519 519
520 520 if end is not None:
521 521 _end = self._get_revision(end)
522 522 try:
523 523 end_pos = revs.index(_end)
524 524 except ValueError:
525 525 pass
526 526
527 527 if None not in [start, end] and start_pos > end_pos:
528 528 raise RepositoryError('start cannot be after end')
529 529
530 530 if end_pos is not None:
531 531 end_pos += 1
532 532
533 533 revs = revs[start_pos:end_pos]
534 534 if reverse:
535 535 revs = reversed(revs)
536 for rev in revs:
537 yield self.get_changeset(rev)
536 return CollectionGenerator(self, revs)
538 537
539 538 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
540 539 context=3):
541 540 """
542 541 Returns (git like) *diff*, as plain text. Shows changes introduced by
543 542 ``rev2`` since ``rev1``.
544 543
545 544 :param rev1: Entry point from which diff is shown. Can be
546 545 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
547 546 the changes since empty state of the repository until ``rev2``
548 547 :param rev2: Until which revision changes should be shown.
549 548 :param ignore_whitespace: If set to ``True``, would not show whitespace
550 549 changes. Defaults to ``False``.
551 550 :param context: How many lines before/after changed lines should be
552 551 shown. Defaults to ``3``.
553 552 """
554 553 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
555 554 if ignore_whitespace:
556 555 flags.append('-w')
557 556
558 557 if hasattr(rev1, 'raw_id'):
559 558 rev1 = getattr(rev1, 'raw_id')
560 559
561 560 if hasattr(rev2, 'raw_id'):
562 561 rev2 = getattr(rev2, 'raw_id')
563 562
564 563 if rev1 == self.EMPTY_CHANGESET:
565 564 rev2 = self.get_changeset(rev2).raw_id
566 565 cmd = ' '.join(['show'] + flags + [rev2])
567 566 else:
568 567 rev1 = self.get_changeset(rev1).raw_id
569 568 rev2 = self.get_changeset(rev2).raw_id
570 569 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
571 570
572 571 if path:
573 572 cmd += ' -- "%s"' % path
574 573
575 574 stdout, stderr = self.run_git_command(cmd)
576 575 # If we used 'show' command, strip first few lines (until actual diff
577 576 # starts)
578 577 if rev1 == self.EMPTY_CHANGESET:
579 578 lines = stdout.splitlines()
580 579 x = 0
581 580 for line in lines:
582 581 if line.startswith('diff'):
583 582 break
584 583 x += 1
585 584 # Append new line just like 'diff' command do
586 585 stdout = '\n'.join(lines[x:]) + '\n'
587 586 return stdout
588 587
589 588 @LazyProperty
590 589 def in_memory_changeset(self):
591 590 """
592 591 Returns ``GitInMemoryChangeset`` object for this repository.
593 592 """
594 593 return GitInMemoryChangeset(self)
595 594
596 595 def clone(self, url, update_after_clone=True, bare=False):
597 596 """
598 597 Tries to clone changes from external location.
599 598
600 599 :param update_after_clone: If set to ``False``, git won't checkout
601 600 working directory
602 601 :param bare: If set to ``True``, repository would be cloned into
603 602 *bare* git repository (no working directory at all).
604 603 """
605 604 url = self._get_url(url)
606 605 cmd = ['clone']
607 606 if bare:
608 607 cmd.append('--bare')
609 608 elif not update_after_clone:
610 609 cmd.append('--no-checkout')
611 610 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
612 611 cmd = ' '.join(cmd)
613 612 # If error occurs run_git_command raises RepositoryError already
614 613 self.run_git_command(cmd)
615 614
616 615 def pull(self, url):
617 616 """
618 617 Tries to pull changes from external location.
619 618 """
620 619 url = self._get_url(url)
621 620 cmd = ['pull']
622 621 cmd.append("--ff-only")
623 622 cmd.append(url)
624 623 cmd = ' '.join(cmd)
625 624 # If error occurs run_git_command raises RepositoryError already
626 625 self.run_git_command(cmd)
627 626
628 627 def fetch(self, url):
629 628 """
630 629 Tries to pull changes from external location.
631 630 """
632 631 url = self._get_url(url)
633 632 so, se = self.run_git_command('ls-remote -h %s' % url)
634 633 refs = []
635 634 for line in (x for x in so.splitlines()):
636 635 sha, ref = line.split('\t')
637 636 refs.append(ref)
638 637 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
639 638 cmd = '''fetch %s -- %s''' % (url, refs)
640 639 self.run_git_command(cmd)
641 640
642 641 @LazyProperty
643 642 def workdir(self):
644 643 """
645 644 Returns ``Workdir`` instance for this repository.
646 645 """
647 646 return GitWorkdir(self)
648 647
649 648 def get_config_value(self, section, name, config_file=None):
650 649 """
651 650 Returns configuration value for a given [``section``] and ``name``.
652 651
653 652 :param section: Section we want to retrieve value from
654 653 :param name: Name of configuration we want to retrieve
655 654 :param config_file: A path to file which should be used to retrieve
656 655 configuration from (might also be a list of file paths)
657 656 """
658 657 if config_file is None:
659 658 config_file = []
660 659 elif isinstance(config_file, basestring):
661 660 config_file = [config_file]
662 661
663 662 def gen_configs():
664 663 for path in config_file + self._config_files:
665 664 try:
666 665 yield ConfigFile.from_path(path)
667 666 except (IOError, OSError, ValueError):
668 667 continue
669 668
670 669 for config in gen_configs():
671 670 try:
672 671 return config.get(section, name)
673 672 except KeyError:
674 673 continue
675 674 return None
676 675
677 676 def get_user_name(self, config_file=None):
678 677 """
679 678 Returns user's name from global configuration file.
680 679
681 680 :param config_file: A path to file which should be used to retrieve
682 681 configuration from (might also be a list of file paths)
683 682 """
684 683 return self.get_config_value('user', 'name', config_file)
685 684
686 685 def get_user_email(self, config_file=None):
687 686 """
688 687 Returns user's email from global configuration file.
689 688
690 689 :param config_file: A path to file which should be used to retrieve
691 690 configuration from (might also be a list of file paths)
692 691 """
693 692 return self.get_config_value('user', 'email', config_file)
@@ -1,555 +1,553
1 1 import os
2 2 import time
3 3 import datetime
4 4 import urllib
5 5 import urllib2
6 6
7 from rhodecode.lib.vcs.backends.base import BaseRepository
7 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
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 rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, \
22 22 diffopts, clone, get_contact, pull, localrepository, RepoLookupError, \
23 23 Abort, RepoError, hex, scmutil
24 24
25 25
26 26 class MercurialRepository(BaseRepository):
27 27 """
28 28 Mercurial repository backend
29 29 """
30 30 DEFAULT_BRANCH_NAME = 'default'
31 31 scm = 'hg'
32 32
33 33 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
34 34 update_after_clone=False):
35 35 """
36 36 Raises RepositoryError if repository could not be find at the given
37 37 ``repo_path``.
38 38
39 39 :param repo_path: local path of the repository
40 40 :param create=False: if set to True, would try to create repository if
41 41 it does not exist rather than raising exception
42 42 :param baseui=None: user data
43 43 :param src_url=None: would try to clone repository from given location
44 44 :param update_after_clone=False: sets update of working copy after
45 45 making a clone
46 46 """
47 47
48 48 if not isinstance(repo_path, str):
49 49 raise VCSError('Mercurial backend requires repository path to '
50 50 'be instance of <str> got %s instead' %
51 51 type(repo_path))
52 52
53 53 self.path = abspath(repo_path)
54 54 self.baseui = baseui or ui.ui()
55 55 # We've set path and ui, now we can set _repo itself
56 56 self._repo = self._get_repo(create, src_url, update_after_clone)
57 57
58 58 @property
59 59 def _empty(self):
60 60 """
61 61 Checks if repository is empty without any changesets
62 62 """
63 63 # TODO: Following raises errors when using InMemoryChangeset...
64 64 # return len(self._repo.changelog) == 0
65 65 return len(self.revisions) == 0
66 66
67 67 @LazyProperty
68 68 def revisions(self):
69 69 """
70 70 Returns list of revisions' ids, in ascending order. Being lazy
71 71 attribute allows external tools to inject shas from cache.
72 72 """
73 73 return self._get_all_revisions()
74 74
75 75 @LazyProperty
76 76 def name(self):
77 77 return os.path.basename(self.path)
78 78
79 79 @LazyProperty
80 80 def branches(self):
81 81 return self._get_branches()
82 82
83 83 @LazyProperty
84 84 def allbranches(self):
85 85 """
86 86 List all branches, including closed branches.
87 87 """
88 88 return self._get_branches(closed=True)
89 89
90 90 def _get_branches(self, closed=False):
91 91 """
92 92 Get's branches for this repository
93 93 Returns only not closed branches by default
94 94
95 95 :param closed: return also closed branches for mercurial
96 96 """
97 97
98 98 if self._empty:
99 99 return {}
100 100
101 101 def _branchtags(localrepo):
102 102 """
103 103 Patched version of mercurial branchtags to not return the closed
104 104 branches
105 105
106 106 :param localrepo: locarepository instance
107 107 """
108 108
109 109 bt = {}
110 110 bt_closed = {}
111 111 for bn, heads in localrepo.branchmap().iteritems():
112 112 tip = heads[-1]
113 113 if 'close' in localrepo.changelog.read(tip)[5]:
114 114 bt_closed[bn] = tip
115 115 else:
116 116 bt[bn] = tip
117 117
118 118 if closed:
119 119 bt.update(bt_closed)
120 120 return bt
121 121
122 122 sortkey = lambda ctx: ctx[0] # sort by name
123 123 _branches = [(safe_unicode(n), hex(h),) for n, h in
124 124 _branchtags(self._repo).items()]
125 125
126 126 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
127 127
128 128 @LazyProperty
129 129 def tags(self):
130 130 """
131 131 Get's tags for this repository
132 132 """
133 133 return self._get_tags()
134 134
135 135 def _get_tags(self):
136 136 if self._empty:
137 137 return {}
138 138
139 139 sortkey = lambda ctx: ctx[0] # sort by name
140 140 _tags = [(safe_unicode(n), hex(h),) for n, h in
141 141 self._repo.tags().items()]
142 142
143 143 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
144 144
145 145 def tag(self, name, user, revision=None, message=None, date=None,
146 146 **kwargs):
147 147 """
148 148 Creates and returns a tag for the given ``revision``.
149 149
150 150 :param name: name for new tag
151 151 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
152 152 :param revision: changeset id for which new tag would be created
153 153 :param message: message of the tag's commit
154 154 :param date: date of tag's commit
155 155
156 156 :raises TagAlreadyExistError: if tag with same name already exists
157 157 """
158 158 if name in self.tags:
159 159 raise TagAlreadyExistError("Tag %s already exists" % name)
160 160 changeset = self.get_changeset(revision)
161 161 local = kwargs.setdefault('local', False)
162 162
163 163 if message is None:
164 164 message = "Added tag %s for changeset %s" % (name,
165 165 changeset.short_id)
166 166
167 167 if date is None:
168 168 date = datetime.datetime.now().ctime()
169 169
170 170 try:
171 171 self._repo.tag(name, changeset._ctx.node(), message, local, user,
172 172 date)
173 173 except Abort, e:
174 174 raise RepositoryError(e.message)
175 175
176 176 # Reinitialize tags
177 177 self.tags = self._get_tags()
178 178 tag_id = self.tags[name]
179 179
180 180 return self.get_changeset(revision=tag_id)
181 181
182 182 def remove_tag(self, name, user, message=None, date=None):
183 183 """
184 184 Removes tag with the given ``name``.
185 185
186 186 :param name: name of the tag to be removed
187 187 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
188 188 :param message: message of the tag's removal commit
189 189 :param date: date of tag's removal commit
190 190
191 191 :raises TagDoesNotExistError: if tag with given name does not exists
192 192 """
193 193 if name not in self.tags:
194 194 raise TagDoesNotExistError("Tag %s does not exist" % name)
195 195 if message is None:
196 196 message = "Removed tag %s" % name
197 197 if date is None:
198 198 date = datetime.datetime.now().ctime()
199 199 local = False
200 200
201 201 try:
202 202 self._repo.tag(name, nullid, message, local, user, date)
203 203 self.tags = self._get_tags()
204 204 except Abort, e:
205 205 raise RepositoryError(e.message)
206 206
207 207 @LazyProperty
208 208 def bookmarks(self):
209 209 """
210 210 Get's bookmarks for this repository
211 211 """
212 212 return self._get_bookmarks()
213 213
214 214 def _get_bookmarks(self):
215 215 if self._empty:
216 216 return {}
217 217
218 218 sortkey = lambda ctx: ctx[0] # sort by name
219 219 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
220 220 self._repo._bookmarks.items()]
221 221 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
222 222
223 223 def _get_all_revisions(self):
224 224
225 225 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
226 226
227 227 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
228 228 context=3):
229 229 """
230 230 Returns (git like) *diff*, as plain text. Shows changes introduced by
231 231 ``rev2`` since ``rev1``.
232 232
233 233 :param rev1: Entry point from which diff is shown. Can be
234 234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
235 235 the changes since empty state of the repository until ``rev2``
236 236 :param rev2: Until which revision changes should be shown.
237 237 :param ignore_whitespace: If set to ``True``, would not show whitespace
238 238 changes. Defaults to ``False``.
239 239 :param context: How many lines before/after changed lines should be
240 240 shown. Defaults to ``3``.
241 241 """
242 242 if hasattr(rev1, 'raw_id'):
243 243 rev1 = getattr(rev1, 'raw_id')
244 244
245 245 if hasattr(rev2, 'raw_id'):
246 246 rev2 = getattr(rev2, 'raw_id')
247 247
248 248 # Check if given revisions are present at repository (may raise
249 249 # ChangesetDoesNotExistError)
250 250 if rev1 != self.EMPTY_CHANGESET:
251 251 self.get_changeset(rev1)
252 252 self.get_changeset(rev2)
253 253 if path:
254 254 file_filter = match(self.path, '', [path])
255 255 else:
256 256 file_filter = None
257 257
258 258 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
259 259 opts=diffopts(git=True,
260 260 ignorews=ignore_whitespace,
261 261 context=context)))
262 262
263 263 @classmethod
264 264 def _check_url(cls, url):
265 265 """
266 266 Function will check given url and try to verify if it's a valid
267 267 link. Sometimes it may happened that mercurial will issue basic
268 268 auth request that can cause whole API to hang when used from python
269 269 or other external calls.
270 270
271 271 On failures it'll raise urllib2.HTTPError, return code 200 if url
272 272 is valid or True if it's a local path
273 273 """
274 274
275 275 from mercurial.util import url as Url
276 276
277 277 # those authnadlers are patched for python 2.6.5 bug an
278 278 # infinit looping when given invalid resources
279 279 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
280 280
281 281 # check first if it's not an local url
282 282 if os.path.isdir(url) or url.startswith('file:'):
283 283 return True
284 284
285 285 if('+' in url[:url.find('://')]):
286 286 url = url[url.find('+') + 1:]
287 287
288 288 handlers = []
289 289 test_uri, authinfo = Url(url).authinfo()
290 290
291 291 if authinfo:
292 292 #create a password manager
293 293 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
294 294 passmgr.add_password(*authinfo)
295 295
296 296 handlers.extend((httpbasicauthhandler(passmgr),
297 297 httpdigestauthhandler(passmgr)))
298 298
299 299 o = urllib2.build_opener(*handlers)
300 300 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
301 301 ('Accept', 'application/mercurial-0.1')]
302 302
303 303 q = {"cmd": 'between'}
304 304 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
305 305 qs = '?%s' % urllib.urlencode(q)
306 306 cu = "%s%s" % (test_uri, qs)
307 307 req = urllib2.Request(cu, None, {})
308 308
309 309 try:
310 310 resp = o.open(req)
311 311 return resp.code == 200
312 312 except Exception, e:
313 313 # means it cannot be cloned
314 314 raise urllib2.URLError("[%s] %s" % (url, e))
315 315
316 316 def _get_repo(self, create, src_url=None, update_after_clone=False):
317 317 """
318 318 Function will check for mercurial repository in given path and return
319 319 a localrepo object. If there is no repository in that path it will
320 320 raise an exception unless ``create`` parameter is set to True - in
321 321 that case repository would be created and returned.
322 322 If ``src_url`` is given, would try to clone repository from the
323 323 location at given clone_point. Additionally it'll make update to
324 324 working copy accordingly to ``update_after_clone`` flag
325 325 """
326 326
327 327 try:
328 328 if src_url:
329 329 url = str(self._get_url(src_url))
330 330 opts = {}
331 331 if not update_after_clone:
332 332 opts.update({'noupdate': True})
333 333 try:
334 334 MercurialRepository._check_url(url)
335 335 clone(self.baseui, url, self.path, **opts)
336 336 # except urllib2.URLError:
337 337 # raise Abort("Got HTTP 404 error")
338 338 except Exception:
339 339 raise
340 340
341 341 # Don't try to create if we've already cloned repo
342 342 create = False
343 343 return localrepository(self.baseui, self.path, create=create)
344 344 except (Abort, RepoError), err:
345 345 if create:
346 346 msg = "Cannot create repository at %s. Original error was %s"\
347 347 % (self.path, err)
348 348 else:
349 349 msg = "Not valid repository at %s. Original error was %s"\
350 350 % (self.path, err)
351 351 raise RepositoryError(msg)
352 352
353 353 @LazyProperty
354 354 def in_memory_changeset(self):
355 355 return MercurialInMemoryChangeset(self)
356 356
357 357 @LazyProperty
358 358 def description(self):
359 359 undefined_description = u'unknown'
360 360 return safe_unicode(self._repo.ui.config('web', 'description',
361 361 undefined_description, untrusted=True))
362 362
363 363 @LazyProperty
364 364 def contact(self):
365 365 undefined_contact = u'Unknown'
366 366 return safe_unicode(get_contact(self._repo.ui.config)
367 367 or undefined_contact)
368 368
369 369 @LazyProperty
370 370 def last_change(self):
371 371 """
372 372 Returns last change made on this repository as datetime object
373 373 """
374 374 return date_fromtimestamp(self._get_mtime(), makedate()[1])
375 375
376 376 def _get_mtime(self):
377 377 try:
378 378 return time.mktime(self.get_changeset().date.timetuple())
379 379 except RepositoryError:
380 380 #fallback to filesystem
381 381 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
382 382 st_path = os.path.join(self.path, '.hg', "store")
383 383 if os.path.exists(cl_path):
384 384 return os.stat(cl_path).st_mtime
385 385 else:
386 386 return os.stat(st_path).st_mtime
387 387
388 388 def _get_hidden(self):
389 389 return self._repo.ui.configbool("web", "hidden", untrusted=True)
390 390
391 391 def _get_revision(self, revision):
392 392 """
393 393 Get's an ID revision given as str. This will always return a fill
394 394 40 char revision number
395 395
396 396 :param revision: str or int or None
397 397 """
398 398
399 399 if self._empty:
400 400 raise EmptyRepositoryError("There are no changesets yet")
401 401
402 402 if revision in [-1, 'tip', None]:
403 403 revision = 'tip'
404 404
405 405 try:
406 406 revision = hex(self._repo.lookup(revision))
407 407 except (IndexError, ValueError, RepoLookupError, TypeError):
408 408 raise ChangesetDoesNotExistError("Revision %s does not "
409 409 "exist for this repository"
410 410 % (revision))
411 411 return revision
412 412
413 413 def _get_archives(self, archive_name='tip'):
414 414 allowed = self.baseui.configlist("web", "allow_archive",
415 415 untrusted=True)
416 416 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
417 417 if i[0] in allowed or self._repo.ui.configbool("web",
418 418 "allow" + i[0],
419 419 untrusted=True):
420 420 yield {"type": i[0], "extension": i[1], "node": archive_name}
421 421
422 422 def _get_url(self, url):
423 423 """
424 424 Returns normalized url. If schema is not given, would fall
425 425 to filesystem
426 426 (``file:///``) schema.
427 427 """
428 428 url = str(url)
429 429 if url != 'default' and not '://' in url:
430 430 url = "file:" + urllib.pathname2url(url)
431 431 return url
432 432
433 433 def get_hook_location(self):
434 434 """
435 435 returns absolute path to location where hooks are stored
436 436 """
437 437 return os.path.join(self.path, '.hg', '.hgrc')
438 438
439 439 def get_changeset(self, revision=None):
440 440 """
441 441 Returns ``MercurialChangeset`` object representing repository's
442 442 changeset at the given ``revision``.
443 443 """
444 444 revision = self._get_revision(revision)
445 445 changeset = MercurialChangeset(repository=self, revision=revision)
446 446 return changeset
447 447
448 448 def get_changesets(self, start=None, end=None, start_date=None,
449 449 end_date=None, branch_name=None, reverse=False):
450 450 """
451 451 Returns iterator of ``MercurialChangeset`` objects from start to end
452 452 (both are inclusive)
453 453
454 454 :param start: None, str, int or mercurial lookup format
455 455 :param end: None, str, int or mercurial lookup format
456 456 :param start_date:
457 457 :param end_date:
458 458 :param branch_name:
459 459 :param reversed: return changesets in reversed order
460 460 """
461 461
462 462 start_raw_id = self._get_revision(start)
463 463 start_pos = self.revisions.index(start_raw_id) if start else None
464 464 end_raw_id = self._get_revision(end)
465 465 end_pos = self.revisions.index(end_raw_id) if end else None
466 466
467 467 if None not in [start, end] and start_pos > end_pos:
468 468 raise RepositoryError("Start revision '%s' cannot be "
469 469 "after end revision '%s'" % (start, end))
470 470
471 471 if branch_name and branch_name not in self.allbranches.keys():
472 472 raise BranchDoesNotExistError('Branch %s not found in'
473 473 ' this repository' % branch_name)
474 474 if end_pos is not None:
475 475 end_pos += 1
476 476 #filter branches
477
477 filter_ = []
478 478 if branch_name:
479 revisions = scmutil.revrange(self._repo,
480 ['branch("%s")' % (branch_name)])
479 filter_.append('branch("%s")' % (branch_name))
480
481 if start_date:
482 filter_.append('date(">%s")' % start_date)
483 if end_date:
484 filter_.append('date("<%s")' % end_date)
485 if filter_:
486 revisions = scmutil.revrange(self._repo, filter_)
481 487 else:
482 488 revisions = self.revisions
483
484 slice_ = reversed(revisions[start_pos:end_pos]) if reverse else \
489 revs = reversed(revisions[start_pos:end_pos]) if reverse else \
485 490 revisions[start_pos:end_pos]
486 491
487 for id_ in slice_:
488 cs = self.get_changeset(id_)
489 if start_date and cs.date < start_date:
490 continue
491 if end_date and cs.date > end_date:
492 continue
493
494 yield cs
492 return CollectionGenerator(self, revs)
495 493
496 494 def pull(self, url):
497 495 """
498 496 Tries to pull changes from external location.
499 497 """
500 498 url = self._get_url(url)
501 499 try:
502 500 pull(self.baseui, self._repo, url)
503 501 except Abort, err:
504 502 # Propagate error but with vcs's type
505 503 raise RepositoryError(str(err))
506 504
507 505 @LazyProperty
508 506 def workdir(self):
509 507 """
510 508 Returns ``Workdir`` instance for this repository.
511 509 """
512 510 return MercurialWorkdir(self)
513 511
514 512 def get_config_value(self, section, name=None, config_file=None):
515 513 """
516 514 Returns configuration value for a given [``section``] and ``name``.
517 515
518 516 :param section: Section we want to retrieve value from
519 517 :param name: Name of configuration we want to retrieve
520 518 :param config_file: A path to file which should be used to retrieve
521 519 configuration from (might also be a list of file paths)
522 520 """
523 521 if config_file is None:
524 522 config_file = []
525 523 elif isinstance(config_file, basestring):
526 524 config_file = [config_file]
527 525
528 526 config = self._repo.ui
529 527 for path in config_file:
530 528 config.readconfig(path)
531 529 return config.config(section, name)
532 530
533 531 def get_user_name(self, config_file=None):
534 532 """
535 533 Returns user's name from global configuration file.
536 534
537 535 :param config_file: A path to file which should be used to retrieve
538 536 configuration from (might also be a list of file paths)
539 537 """
540 538 username = self.get_config_value('ui', 'username')
541 539 if username:
542 540 return author_name(username)
543 541 return None
544 542
545 543 def get_user_email(self, config_file=None):
546 544 """
547 545 Returns user's email from global configuration file.
548 546
549 547 :param config_file: A path to file which should be used to retrieve
550 548 configuration from (might also be a list of file paths)
551 549 """
552 550 username = self.get_config_value('ui', 'username')
553 551 if username:
554 552 return author_email(username)
555 553 return None
General Comments 0
You need to be logged in to leave comments. Login now