##// END OF EJS Templates
fixed issue with web-editor that didn't preserve executable bit...
marcink -
r3837:68331e68 default
parent child Browse files
Show More
@@ -1,619 +1,620 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.nodes
4 4 ~~~~~~~~~
5 5
6 6 Module holding everything related to vcs nodes.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11 import os
12 12 import stat
13 13 import posixpath
14 14 import mimetypes
15 15
16 16 from pygments import lexers
17 17
18 18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 19 from rhodecode.lib.vcs.utils import safe_unicode
20 20 from rhodecode.lib.vcs.exceptions import NodeError
21 21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
23 23
24 24
25 25 class NodeKind:
26 26 SUBMODULE = -1
27 27 DIR = 1
28 28 FILE = 2
29 29
30 30
31 31 class NodeState:
32 32 ADDED = u'added'
33 33 CHANGED = u'changed'
34 34 NOT_CHANGED = u'not changed'
35 35 REMOVED = u'removed'
36 36
37 37
38 38 class NodeGeneratorBase(object):
39 39 """
40 40 Base class for removed added and changed filenodes, it's a lazy generator
41 41 class that will create filenodes only on iteration or call
42 42
43 43 The len method doesn't need to create filenodes at all
44 44 """
45 45
46 46 def __init__(self, current_paths, cs):
47 47 self.cs = cs
48 48 self.current_paths = current_paths
49 49
50 50 def __call__(self):
51 51 return [n for n in self]
52 52
53 53 def __getslice__(self, i, j):
54 54 for p in self.current_paths[i:j]:
55 55 yield self.cs.get_node(p)
56 56
57 57 def __len__(self):
58 58 return len(self.current_paths)
59 59
60 60 def __iter__(self):
61 61 for p in self.current_paths:
62 62 yield self.cs.get_node(p)
63 63
64 64
65 65 class AddedFileNodesGenerator(NodeGeneratorBase):
66 66 """
67 67 Class holding Added files for current changeset
68 68 """
69 69 pass
70 70
71 71
72 72 class ChangedFileNodesGenerator(NodeGeneratorBase):
73 73 """
74 74 Class holding Changed files for current changeset
75 75 """
76 76 pass
77 77
78 78
79 79 class RemovedFileNodesGenerator(NodeGeneratorBase):
80 80 """
81 81 Class holding removed files for current changeset
82 82 """
83 83 def __iter__(self):
84 84 for p in self.current_paths:
85 85 yield RemovedFileNode(path=p)
86 86
87 87 def __getslice__(self, i, j):
88 88 for p in self.current_paths[i:j]:
89 89 yield RemovedFileNode(path=p)
90 90
91 91
92 92 class Node(object):
93 93 """
94 94 Simplest class representing file or directory on repository. SCM backends
95 95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
96 96 directly.
97 97
98 98 Node's ``path`` cannot start with slash as we operate on *relative* paths
99 99 only. Moreover, every single node is identified by the ``path`` attribute,
100 100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
101 101 """
102 102
103 103 def __init__(self, path, kind):
104 104 if path.startswith('/'):
105 105 raise NodeError("Cannot initialize Node objects with slash at "
106 106 "the beginning as only relative paths are supported")
107 107 self.path = path.rstrip('/')
108 108 if path == '' and kind != NodeKind.DIR:
109 109 raise NodeError("Only DirNode and its subclasses may be "
110 110 "initialized with empty path")
111 111 self.kind = kind
112 112 #self.dirs, self.files = [], []
113 113 if self.is_root() and not self.is_dir():
114 114 raise NodeError("Root node cannot be FILE kind")
115 115
116 116 @LazyProperty
117 117 def parent(self):
118 118 parent_path = self.get_parent_path()
119 119 if parent_path:
120 120 if self.changeset:
121 121 return self.changeset.get_node(parent_path)
122 122 return DirNode(parent_path)
123 123 return None
124 124
125 125 @LazyProperty
126 126 def unicode_path(self):
127 127 return safe_unicode(self.path)
128 128
129 129 @LazyProperty
130 130 def name(self):
131 131 """
132 132 Returns name of the node so if its path
133 133 then only last part is returned.
134 134 """
135 135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
136 136
137 137 def _get_kind(self):
138 138 return self._kind
139 139
140 140 def _set_kind(self, kind):
141 141 if hasattr(self, '_kind'):
142 142 raise NodeError("Cannot change node's kind")
143 143 else:
144 144 self._kind = kind
145 145 # Post setter check (path's trailing slash)
146 146 if self.path.endswith('/'):
147 147 raise NodeError("Node's path cannot end with slash")
148 148
149 149 kind = property(_get_kind, _set_kind)
150 150
151 151 def __cmp__(self, other):
152 152 """
153 153 Comparator using name of the node, needed for quick list sorting.
154 154 """
155 155 kind_cmp = cmp(self.kind, other.kind)
156 156 if kind_cmp:
157 157 return kind_cmp
158 158 return cmp(self.name, other.name)
159 159
160 160 def __eq__(self, other):
161 161 for attr in ['name', 'path', 'kind']:
162 162 if getattr(self, attr) != getattr(other, attr):
163 163 return False
164 164 if self.is_file():
165 165 if self.content != other.content:
166 166 return False
167 167 else:
168 168 # For DirNode's check without entering each dir
169 169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
170 170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
171 171 if self_nodes_paths != other_nodes_paths:
172 172 return False
173 173 return True
174 174
175 175 def __nq__(self, other):
176 176 return not self.__eq__(other)
177 177
178 178 def __repr__(self):
179 179 return '<%s %r>' % (self.__class__.__name__, self.path)
180 180
181 181 def __str__(self):
182 182 return self.__repr__()
183 183
184 184 def __unicode__(self):
185 185 return self.name
186 186
187 187 def get_parent_path(self):
188 188 """
189 189 Returns node's parent path or empty string if node is root.
190 190 """
191 191 if self.is_root():
192 192 return ''
193 193 return posixpath.dirname(self.path.rstrip('/')) + '/'
194 194
195 195 def is_file(self):
196 196 """
197 197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
198 198 otherwise.
199 199 """
200 200 return self.kind == NodeKind.FILE
201 201
202 202 def is_dir(self):
203 203 """
204 204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
205 205 otherwise.
206 206 """
207 207 return self.kind == NodeKind.DIR
208 208
209 209 def is_root(self):
210 210 """
211 211 Returns ``True`` if node is a root node and ``False`` otherwise.
212 212 """
213 213 return self.kind == NodeKind.DIR and self.path == ''
214 214
215 215 def is_submodule(self):
216 216 """
217 217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
218 218 otherwise.
219 219 """
220 220 return self.kind == NodeKind.SUBMODULE
221 221
222 222 @LazyProperty
223 223 def added(self):
224 224 return self.state is NodeState.ADDED
225 225
226 226 @LazyProperty
227 227 def changed(self):
228 228 return self.state is NodeState.CHANGED
229 229
230 230 @LazyProperty
231 231 def not_changed(self):
232 232 return self.state is NodeState.NOT_CHANGED
233 233
234 234 @LazyProperty
235 235 def removed(self):
236 236 return self.state is NodeState.REMOVED
237 237
238 238
239 239 class FileNode(Node):
240 240 """
241 241 Class representing file nodes.
242 242
243 243 :attribute: path: path to the node, relative to repostiory's root
244 244 :attribute: content: if given arbitrary sets content of the file
245 245 :attribute: changeset: if given, first time content is accessed, callback
246 246 :attribute: mode: octal stat mode for a node. Default is 0100644.
247 247 """
248 248
249 249 def __init__(self, path, content=None, changeset=None, mode=None):
250 250 """
251 251 Only one of ``content`` and ``changeset`` may be given. Passing both
252 252 would raise ``NodeError`` exception.
253 253
254 254 :param path: relative path to the node
255 255 :param content: content may be passed to constructor
256 256 :param changeset: if given, will use it to lazily fetch content
257 257 :param mode: octal representation of ST_MODE (i.e. 0100644)
258 258 """
259 259
260 260 if content and changeset:
261 261 raise NodeError("Cannot use both content and changeset")
262 262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
263 263 self.changeset = changeset
264 264 self._content = content
265 265 self._mode = mode or 0100644
266 266
267 267 @LazyProperty
268 268 def mode(self):
269 269 """
270 270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
271 271 use value given at initialization or 0100644 (default).
272 272 """
273 273 if self.changeset:
274 274 mode = self.changeset.get_file_mode(self.path)
275 275 else:
276 276 mode = self._mode
277 277 return mode
278 278
279 279 def _get_content(self):
280 280 if self.changeset:
281 281 content = self.changeset.get_file_content(self.path)
282 282 else:
283 283 content = self._content
284 284 return content
285 285
286 286 @property
287 287 def content(self):
288 288 """
289 289 Returns lazily content of the FileNode. If possible, would try to
290 290 decode content from UTF-8.
291 291 """
292 292 content = self._get_content()
293 293
294 294 if bool(content and '\0' in content):
295 295 return content
296 296 return safe_unicode(content)
297 297
298 298 @LazyProperty
299 299 def size(self):
300 300 if self.changeset:
301 301 return self.changeset.get_file_size(self.path)
302 302 raise NodeError("Cannot retrieve size of the file without related "
303 303 "changeset attribute")
304 304
305 305 @LazyProperty
306 306 def message(self):
307 307 if self.changeset:
308 308 return self.last_changeset.message
309 309 raise NodeError("Cannot retrieve message of the file without related "
310 310 "changeset attribute")
311 311
312 312 @LazyProperty
313 313 def last_changeset(self):
314 314 if self.changeset:
315 315 return self.changeset.get_file_changeset(self.path)
316 316 raise NodeError("Cannot retrieve last changeset of the file without "
317 317 "related changeset attribute")
318 318
319 319 def get_mimetype(self):
320 320 """
321 321 Mimetype is calculated based on the file's content. If ``_mimetype``
322 322 attribute is available, it will be returned (backends which store
323 323 mimetypes or can easily recognize them, should set this private
324 324 attribute to indicate that type should *NOT* be calculated).
325 325 """
326 326 if hasattr(self, '_mimetype'):
327 327 if (isinstance(self._mimetype, (tuple, list,)) and
328 328 len(self._mimetype) == 2):
329 329 return self._mimetype
330 330 else:
331 331 raise NodeError('given _mimetype attribute must be an 2 '
332 332 'element list or tuple')
333 333
334 334 mtype, encoding = mimetypes.guess_type(self.name)
335 335
336 336 if mtype is None:
337 337 if self.is_binary:
338 338 mtype = 'application/octet-stream'
339 339 encoding = None
340 340 else:
341 341 mtype = 'text/plain'
342 342 encoding = None
343 343 return mtype, encoding
344 344
345 345 @LazyProperty
346 346 def mimetype(self):
347 347 """
348 348 Wrapper around full mimetype info. It returns only type of fetched
349 349 mimetype without the encoding part. use get_mimetype function to fetch
350 350 full set of (type,encoding)
351 351 """
352 352 return self.get_mimetype()[0]
353 353
354 354 @LazyProperty
355 355 def mimetype_main(self):
356 356 return ['', '']
357 357 return self.mimetype.split('/')[0]
358 358
359 359 @LazyProperty
360 360 def lexer(self):
361 361 """
362 362 Returns pygment's lexer class. Would try to guess lexer taking file's
363 363 content, name and mimetype.
364 364 """
365 365
366 366 try:
367 367 lexer = lexers.guess_lexer_for_filename(self.name, self.content, stripnl=False)
368 368 except lexers.ClassNotFound:
369 369 lexer = lexers.TextLexer(stripnl=False)
370 370 # returns first alias
371 371 return lexer
372 372
373 373 @LazyProperty
374 374 def lexer_alias(self):
375 375 """
376 376 Returns first alias of the lexer guessed for this file.
377 377 """
378 378 return self.lexer.aliases[0]
379 379
380 380 @LazyProperty
381 381 def history(self):
382 382 """
383 383 Returns a list of changeset for this file in which the file was changed
384 384 """
385 385 if self.changeset is None:
386 386 raise NodeError('Unable to get changeset for this FileNode')
387 387 return self.changeset.get_file_history(self.path)
388 388
389 389 @LazyProperty
390 390 def annotate(self):
391 391 """
392 392 Returns a list of three element tuples with lineno,changeset and line
393 393 """
394 394 if self.changeset is None:
395 395 raise NodeError('Unable to get changeset for this FileNode')
396 396 return self.changeset.get_file_annotate(self.path)
397 397
398 398 @LazyProperty
399 399 def state(self):
400 400 if not self.changeset:
401 401 raise NodeError("Cannot check state of the node if it's not "
402 402 "linked with changeset")
403 403 elif self.path in (node.path for node in self.changeset.added):
404 404 return NodeState.ADDED
405 405 elif self.path in (node.path for node in self.changeset.changed):
406 406 return NodeState.CHANGED
407 407 else:
408 408 return NodeState.NOT_CHANGED
409 409
410 410 @property
411 411 def is_binary(self):
412 412 """
413 413 Returns True if file has binary content.
414 414 """
415 415 _bin = '\0' in self._get_content()
416 416 return _bin
417 417
418 418 @LazyProperty
419 419 def extension(self):
420 420 """Returns filenode extension"""
421 421 return self.name.split('.')[-1]
422 422
423 @property
423 424 def is_executable(self):
424 425 """
425 426 Returns ``True`` if file has executable flag turned on.
426 427 """
427 428 return bool(self.mode & stat.S_IXUSR)
428 429
429 430 def __repr__(self):
430 431 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
431 432 getattr(self.changeset, 'short_id', ''))
432 433
433 434
434 435 class RemovedFileNode(FileNode):
435 436 """
436 437 Dummy FileNode class - trying to access any public attribute except path,
437 438 name, kind or state (or methods/attributes checking those two) would raise
438 439 RemovedFileNodeError.
439 440 """
440 441 ALLOWED_ATTRIBUTES = [
441 442 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
442 443 'added', 'changed', 'not_changed', 'removed'
443 444 ]
444 445
445 446 def __init__(self, path):
446 447 """
447 448 :param path: relative path to the node
448 449 """
449 450 super(RemovedFileNode, self).__init__(path=path)
450 451
451 452 def __getattribute__(self, attr):
452 453 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
453 454 return super(RemovedFileNode, self).__getattribute__(attr)
454 455 raise RemovedFileNodeError("Cannot access attribute %s on "
455 456 "RemovedFileNode" % attr)
456 457
457 458 @LazyProperty
458 459 def state(self):
459 460 return NodeState.REMOVED
460 461
461 462
462 463 class DirNode(Node):
463 464 """
464 465 DirNode stores list of files and directories within this node.
465 466 Nodes may be used standalone but within repository context they
466 467 lazily fetch data within same repositorty's changeset.
467 468 """
468 469
469 470 def __init__(self, path, nodes=(), changeset=None):
470 471 """
471 472 Only one of ``nodes`` and ``changeset`` may be given. Passing both
472 473 would raise ``NodeError`` exception.
473 474
474 475 :param path: relative path to the node
475 476 :param nodes: content may be passed to constructor
476 477 :param changeset: if given, will use it to lazily fetch content
477 478 :param size: always 0 for ``DirNode``
478 479 """
479 480 if nodes and changeset:
480 481 raise NodeError("Cannot use both nodes and changeset")
481 482 super(DirNode, self).__init__(path, NodeKind.DIR)
482 483 self.changeset = changeset
483 484 self._nodes = nodes
484 485
485 486 @LazyProperty
486 487 def content(self):
487 488 raise NodeError("%s represents a dir and has no ``content`` attribute"
488 489 % self)
489 490
490 491 @LazyProperty
491 492 def nodes(self):
492 493 if self.changeset:
493 494 nodes = self.changeset.get_nodes(self.path)
494 495 else:
495 496 nodes = self._nodes
496 497 self._nodes_dict = dict((node.path, node) for node in nodes)
497 498 return sorted(nodes)
498 499
499 500 @LazyProperty
500 501 def files(self):
501 502 return sorted((node for node in self.nodes if node.is_file()))
502 503
503 504 @LazyProperty
504 505 def dirs(self):
505 506 return sorted((node for node in self.nodes if node.is_dir()))
506 507
507 508 def __iter__(self):
508 509 for node in self.nodes:
509 510 yield node
510 511
511 512 def get_node(self, path):
512 513 """
513 514 Returns node from within this particular ``DirNode``, so it is now
514 515 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
515 516 'docs'. In order to access deeper nodes one must fetch nodes between
516 517 them first - this would work::
517 518
518 519 docs = root.get_node('docs')
519 520 docs.get_node('api').get_node('index.rst')
520 521
521 522 :param: path - relative to the current node
522 523
523 524 .. note::
524 525 To access lazily (as in example above) node have to be initialized
525 526 with related changeset object - without it node is out of
526 527 context and may know nothing about anything else than nearest
527 528 (located at same level) nodes.
528 529 """
529 530 try:
530 531 path = path.rstrip('/')
531 532 if path == '':
532 533 raise NodeError("Cannot retrieve node without path")
533 534 self.nodes # access nodes first in order to set _nodes_dict
534 535 paths = path.split('/')
535 536 if len(paths) == 1:
536 537 if not self.is_root():
537 538 path = '/'.join((self.path, paths[0]))
538 539 else:
539 540 path = paths[0]
540 541 return self._nodes_dict[path]
541 542 elif len(paths) > 1:
542 543 if self.changeset is None:
543 544 raise NodeError("Cannot access deeper "
544 545 "nodes without changeset")
545 546 else:
546 547 path1, path2 = paths[0], '/'.join(paths[1:])
547 548 return self.get_node(path1).get_node(path2)
548 549 else:
549 550 raise KeyError
550 551 except KeyError:
551 552 raise NodeError("Node does not exist at %s" % path)
552 553
553 554 @LazyProperty
554 555 def state(self):
555 556 raise NodeError("Cannot access state of DirNode")
556 557
557 558 @LazyProperty
558 559 def size(self):
559 560 size = 0
560 561 for root, dirs, files in self.changeset.walk(self.path):
561 562 for f in files:
562 563 size += f.size
563 564
564 565 return size
565 566
566 567 def __repr__(self):
567 568 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
568 569 getattr(self.changeset, 'short_id', ''))
569 570
570 571
571 572 class RootNode(DirNode):
572 573 """
573 574 DirNode being the root node of the repository.
574 575 """
575 576
576 577 def __init__(self, nodes=(), changeset=None):
577 578 super(RootNode, self).__init__(path='', nodes=nodes,
578 579 changeset=changeset)
579 580
580 581 def __repr__(self):
581 582 return '<%s>' % self.__class__.__name__
582 583
583 584
584 585 class SubModuleNode(Node):
585 586 """
586 587 represents a SubModule of Git or SubRepo of Mercurial
587 588 """
588 589 is_binary = False
589 590 size = 0
590 591
591 592 def __init__(self, name, url=None, changeset=None, alias=None):
592 593 self.path = name
593 594 self.kind = NodeKind.SUBMODULE
594 595 self.alias = alias
595 596 # we have to use emptyChangeset here since this can point to svn/git/hg
596 597 # submodules we cannot get from repository
597 598 self.changeset = EmptyChangeset(str(changeset), alias=alias)
598 599 self.url = url or self._extract_submodule_url()
599 600
600 601 def __repr__(self):
601 602 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
602 603 getattr(self.changeset, 'short_id', ''))
603 604
604 605 def _extract_submodule_url(self):
605 606 if self.alias == 'git':
606 607 #TODO: find a way to parse gits submodule file and extract the
607 608 # linking URL
608 609 return self.path
609 610 if self.alias == 'hg':
610 611 return self.path
611 612
612 613 @LazyProperty
613 614 def name(self):
614 615 """
615 616 Returns name of the node so if its path
616 617 then only last part is returned.
617 618 """
618 619 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
619 620 return u'%s @ %s' % (org, self.changeset.short_id)
@@ -1,674 +1,674 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 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 from __future__ import with_statement
26 26 import os
27 27 import re
28 28 import time
29 29 import traceback
30 30 import logging
31 31 import cStringIO
32 32 import pkg_resources
33 33 from os.path import dirname as dn, join as jn
34 34
35 35 from sqlalchemy import func
36 36 from pylons.i18n.translation import _
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 42 from rhodecode.lib.vcs.nodes import FileNode
43 43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 44
45 45 from rhodecode import BACKENDS
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url,\
48 48 _set_extras
49 49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
50 50 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
51 51 action_logger, REMOVED_REPO_PAT
52 52 from rhodecode.model import BaseModel
53 53 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
54 54 UserFollowing, UserLog, User, RepoGroup, PullRequest
55 55 from rhodecode.lib.hooks import log_push_action
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class UserTemp(object):
61 61 def __init__(self, user_id):
62 62 self.user_id = user_id
63 63
64 64 def __repr__(self):
65 65 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
66 66
67 67
68 68 class RepoTemp(object):
69 69 def __init__(self, repo_id):
70 70 self.repo_id = repo_id
71 71
72 72 def __repr__(self):
73 73 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
74 74
75 75
76 76 class CachedRepoList(object):
77 77 """
78 78 Cached repo list, uses in-memory cache after initialization, that is
79 79 super fast
80 80 """
81 81
82 82 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
83 83 self.db_repo_list = db_repo_list
84 84 self.repos_path = repos_path
85 85 self.order_by = order_by
86 86 self.reversed = (order_by or '').startswith('-')
87 87 if not perm_set:
88 88 perm_set = ['repository.read', 'repository.write',
89 89 'repository.admin']
90 90 self.perm_set = perm_set
91 91
92 92 def __len__(self):
93 93 return len(self.db_repo_list)
94 94
95 95 def __repr__(self):
96 96 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
97 97
98 98 def __iter__(self):
99 99 # pre-propagated cache_map to save executing select statements
100 100 # for each repo
101 101 cache_map = CacheInvalidation.get_cache_map()
102 102
103 103 for dbr in self.db_repo_list:
104 104 scmr = dbr.scm_instance_cached(cache_map)
105 105 # check permission at this level
106 106 if not HasRepoPermissionAny(
107 107 *self.perm_set
108 108 )(dbr.repo_name, 'get repo check'):
109 109 continue
110 110
111 111 try:
112 112 last_change = scmr.last_change
113 113 tip = h.get_changeset_safe(scmr, 'tip')
114 114 except Exception:
115 115 log.error(
116 116 '%s this repository is present in database but it '
117 117 'cannot be created as an scm instance, org_exc:%s'
118 118 % (dbr.repo_name, traceback.format_exc())
119 119 )
120 120 continue
121 121
122 122 tmp_d = {}
123 123 tmp_d['name'] = dbr.repo_name
124 124 tmp_d['name_sort'] = tmp_d['name'].lower()
125 125 tmp_d['raw_name'] = tmp_d['name'].lower()
126 126 tmp_d['description'] = dbr.description
127 127 tmp_d['description_sort'] = tmp_d['description'].lower()
128 128 tmp_d['last_change'] = last_change
129 129 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
130 130 tmp_d['tip'] = tip.raw_id
131 131 tmp_d['tip_sort'] = tip.revision
132 132 tmp_d['rev'] = tip.revision
133 133 tmp_d['contact'] = dbr.user.full_contact
134 134 tmp_d['contact_sort'] = tmp_d['contact']
135 135 tmp_d['owner_sort'] = tmp_d['contact']
136 136 tmp_d['repo_archives'] = list(scmr._get_archives())
137 137 tmp_d['last_msg'] = tip.message
138 138 tmp_d['author'] = tip.author
139 139 tmp_d['dbrepo'] = dbr.get_dict()
140 140 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
141 141 yield tmp_d
142 142
143 143
144 144 class SimpleCachedRepoList(CachedRepoList):
145 145 """
146 146 Lighter version of CachedRepoList without the scm initialisation
147 147 """
148 148
149 149 def __iter__(self):
150 150 for dbr in self.db_repo_list:
151 151 # check permission at this level
152 152 if not HasRepoPermissionAny(
153 153 *self.perm_set
154 154 )(dbr.repo_name, 'get repo check'):
155 155 continue
156 156
157 157 tmp_d = {}
158 158 tmp_d['name'] = dbr.repo_name
159 159 tmp_d['name_sort'] = tmp_d['name'].lower()
160 160 tmp_d['raw_name'] = tmp_d['name'].lower()
161 161 tmp_d['description'] = dbr.description
162 162 tmp_d['description_sort'] = tmp_d['description'].lower()
163 163 tmp_d['dbrepo'] = dbr.get_dict()
164 164 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
165 165 yield tmp_d
166 166
167 167
168 168 class GroupList(object):
169 169
170 170 def __init__(self, db_repo_group_list, perm_set=None):
171 171 """
172 172 Creates iterator from given list of group objects, additionally
173 173 checking permission for them from perm_set var
174 174
175 175 :param db_repo_group_list:
176 176 :param perm_set: list of permissons to check
177 177 """
178 178 self.db_repo_group_list = db_repo_group_list
179 179 if not perm_set:
180 180 perm_set = ['group.read', 'group.write', 'group.admin']
181 181 self.perm_set = perm_set
182 182
183 183 def __len__(self):
184 184 return len(self.db_repo_group_list)
185 185
186 186 def __repr__(self):
187 187 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
188 188
189 189 def __iter__(self):
190 190 for dbgr in self.db_repo_group_list:
191 191 # check permission at this level
192 192 if not HasReposGroupPermissionAny(
193 193 *self.perm_set
194 194 )(dbgr.group_name, 'get group repo check'):
195 195 continue
196 196
197 197 yield dbgr
198 198
199 199
200 200 class ScmModel(BaseModel):
201 201 """
202 202 Generic Scm Model
203 203 """
204 204
205 205 def __get_repo(self, instance):
206 206 cls = Repository
207 207 if isinstance(instance, cls):
208 208 return instance
209 209 elif isinstance(instance, int) or safe_str(instance).isdigit():
210 210 return cls.get(instance)
211 211 elif isinstance(instance, basestring):
212 212 return cls.get_by_repo_name(instance)
213 213 elif instance:
214 214 raise Exception('given object must be int, basestr or Instance'
215 215 ' of %s got %s' % (type(cls), type(instance)))
216 216
217 217 @LazyProperty
218 218 def repos_path(self):
219 219 """
220 220 Get's the repositories root path from database
221 221 """
222 222
223 223 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
224 224
225 225 return q.ui_value
226 226
227 227 def repo_scan(self, repos_path=None):
228 228 """
229 229 Listing of repositories in given path. This path should not be a
230 230 repository itself. Return a dictionary of repository objects
231 231
232 232 :param repos_path: path to directory containing repositories
233 233 """
234 234
235 235 if repos_path is None:
236 236 repos_path = self.repos_path
237 237
238 238 log.info('scanning for repositories in %s' % repos_path)
239 239
240 240 baseui = make_ui('db')
241 241 repos = {}
242 242
243 243 for name, path in get_filesystem_repos(repos_path, recursive=True):
244 244 # name need to be decomposed and put back together using the /
245 245 # since this is internal storage separator for rhodecode
246 246 name = Repository.normalize_repo_name(name)
247 247
248 248 try:
249 249 if name in repos:
250 250 raise RepositoryError('Duplicate repository name %s '
251 251 'found in %s' % (name, path))
252 252 else:
253 253
254 254 klass = get_backend(path[0])
255 255
256 256 if path[0] == 'hg' and path[0] in BACKENDS.keys():
257 257 repos[name] = klass(safe_str(path[1]), baseui=baseui)
258 258
259 259 if path[0] == 'git' and path[0] in BACKENDS.keys():
260 260 repos[name] = klass(path[1])
261 261 except OSError:
262 262 continue
263 263 log.debug('found %s paths with repositories' % (len(repos)))
264 264 return repos
265 265
266 266 def get_repos(self, all_repos=None, sort_key=None, simple=False):
267 267 """
268 268 Get all repos from db and for each repo create it's
269 269 backend instance and fill that backed with information from database
270 270
271 271 :param all_repos: list of repository names as strings
272 272 give specific repositories list, good for filtering
273 273
274 274 :param sort_key: initial sorting of repos
275 275 :param simple: use SimpleCachedList - one without the SCM info
276 276 """
277 277 if all_repos is None:
278 278 all_repos = self.sa.query(Repository)\
279 279 .filter(Repository.group_id == None)\
280 280 .order_by(func.lower(Repository.repo_name)).all()
281 281 if simple:
282 282 repo_iter = SimpleCachedRepoList(all_repos,
283 283 repos_path=self.repos_path,
284 284 order_by=sort_key)
285 285 else:
286 286 repo_iter = CachedRepoList(all_repos,
287 287 repos_path=self.repos_path,
288 288 order_by=sort_key)
289 289
290 290 return repo_iter
291 291
292 292 def get_repos_groups(self, all_groups=None):
293 293 if all_groups is None:
294 294 all_groups = RepoGroup.query()\
295 295 .filter(RepoGroup.group_parent_id == None).all()
296 296 return [x for x in GroupList(all_groups)]
297 297
298 298 def mark_for_invalidation(self, repo_name):
299 299 """
300 300 Puts cache invalidation task into db for
301 301 further global cache invalidation
302 302
303 303 :param repo_name: this repo that should invalidation take place
304 304 """
305 305 invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
306 306 repo = Repository.get_by_repo_name(repo_name)
307 307 if repo:
308 308 repo.update_changeset_cache()
309 309 return invalidated_keys
310 310
311 311 def toggle_following_repo(self, follow_repo_id, user_id):
312 312
313 313 f = self.sa.query(UserFollowing)\
314 314 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
315 315 .filter(UserFollowing.user_id == user_id).scalar()
316 316
317 317 if f is not None:
318 318 try:
319 319 self.sa.delete(f)
320 320 action_logger(UserTemp(user_id),
321 321 'stopped_following_repo',
322 322 RepoTemp(follow_repo_id))
323 323 return
324 324 except Exception:
325 325 log.error(traceback.format_exc())
326 326 raise
327 327
328 328 try:
329 329 f = UserFollowing()
330 330 f.user_id = user_id
331 331 f.follows_repo_id = follow_repo_id
332 332 self.sa.add(f)
333 333
334 334 action_logger(UserTemp(user_id),
335 335 'started_following_repo',
336 336 RepoTemp(follow_repo_id))
337 337 except Exception:
338 338 log.error(traceback.format_exc())
339 339 raise
340 340
341 341 def toggle_following_user(self, follow_user_id, user_id):
342 342 f = self.sa.query(UserFollowing)\
343 343 .filter(UserFollowing.follows_user_id == follow_user_id)\
344 344 .filter(UserFollowing.user_id == user_id).scalar()
345 345
346 346 if f is not None:
347 347 try:
348 348 self.sa.delete(f)
349 349 return
350 350 except Exception:
351 351 log.error(traceback.format_exc())
352 352 raise
353 353
354 354 try:
355 355 f = UserFollowing()
356 356 f.user_id = user_id
357 357 f.follows_user_id = follow_user_id
358 358 self.sa.add(f)
359 359 except Exception:
360 360 log.error(traceback.format_exc())
361 361 raise
362 362
363 363 def is_following_repo(self, repo_name, user_id, cache=False):
364 364 r = self.sa.query(Repository)\
365 365 .filter(Repository.repo_name == repo_name).scalar()
366 366
367 367 f = self.sa.query(UserFollowing)\
368 368 .filter(UserFollowing.follows_repository == r)\
369 369 .filter(UserFollowing.user_id == user_id).scalar()
370 370
371 371 return f is not None
372 372
373 373 def is_following_user(self, username, user_id, cache=False):
374 374 u = User.get_by_username(username)
375 375
376 376 f = self.sa.query(UserFollowing)\
377 377 .filter(UserFollowing.follows_user == u)\
378 378 .filter(UserFollowing.user_id == user_id).scalar()
379 379
380 380 return f is not None
381 381
382 382 def get_followers(self, repo):
383 383 repo = self._get_repo(repo)
384 384
385 385 return self.sa.query(UserFollowing)\
386 386 .filter(UserFollowing.follows_repository == repo).count()
387 387
388 388 def get_forks(self, repo):
389 389 repo = self._get_repo(repo)
390 390 return self.sa.query(Repository)\
391 391 .filter(Repository.fork == repo).count()
392 392
393 393 def get_pull_requests(self, repo):
394 394 repo = self._get_repo(repo)
395 395 return self.sa.query(PullRequest)\
396 396 .filter(PullRequest.other_repo == repo)\
397 397 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
398 398
399 399 def mark_as_fork(self, repo, fork, user):
400 400 repo = self.__get_repo(repo)
401 401 fork = self.__get_repo(fork)
402 402 if fork and repo.repo_id == fork.repo_id:
403 403 raise Exception("Cannot set repository as fork of itself")
404 404 repo.fork = fork
405 405 self.sa.add(repo)
406 406 return repo
407 407
408 408 def _handle_push(self, repo, username, action, repo_name, revisions):
409 409 """
410 410 Triggers push action hooks
411 411
412 412 :param repo: SCM repo
413 413 :param username: username who pushes
414 414 :param action: push/push_loca/push_remote
415 415 :param repo_name: name of repo
416 416 :param revisions: list of revisions that we pushed
417 417 """
418 418 from rhodecode import CONFIG
419 419 from rhodecode.lib.base import _get_ip_addr
420 420 try:
421 421 from pylons import request
422 422 environ = request.environ
423 423 except TypeError:
424 424 # we might use this outside of request context, let's fake the
425 425 # environ data
426 426 from webob import Request
427 427 environ = Request.blank('').environ
428 428
429 429 #trigger push hook
430 430 extras = {
431 431 'ip': _get_ip_addr(environ),
432 432 'username': username,
433 433 'action': 'push_local',
434 434 'repository': repo_name,
435 435 'scm': repo.alias,
436 436 'config': CONFIG['__file__'],
437 437 'server_url': get_server_url(environ),
438 438 'make_lock': None,
439 439 'locked_by': [None, None]
440 440 }
441 441 _scm_repo = repo._repo
442 442 _set_extras(extras)
443 443 if repo.alias == 'hg':
444 444 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
445 445 elif repo.alias == 'git':
446 446 log_push_action(None, _scm_repo, _git_revs=revisions)
447 447
448 448 def _get_IMC_module(self, scm_type):
449 449 """
450 450 Returns InMemoryCommit class based on scm_type
451 451
452 452 :param scm_type:
453 453 """
454 454 if scm_type == 'hg':
455 455 from rhodecode.lib.vcs.backends.hg import \
456 456 MercurialInMemoryChangeset as IMC
457 457 elif scm_type == 'git':
458 458 from rhodecode.lib.vcs.backends.git import \
459 459 GitInMemoryChangeset as IMC
460 460 return IMC
461 461
462 462 def pull_changes(self, repo, username):
463 463 dbrepo = self.__get_repo(repo)
464 464 clone_uri = dbrepo.clone_uri
465 465 if not clone_uri:
466 466 raise Exception("This repository doesn't have a clone uri")
467 467
468 468 repo = dbrepo.scm_instance
469 469 repo_name = dbrepo.repo_name
470 470 try:
471 471 if repo.alias == 'git':
472 472 repo.fetch(clone_uri)
473 473 else:
474 474 repo.pull(clone_uri)
475 475 self.mark_for_invalidation(repo_name)
476 476 except Exception:
477 477 log.error(traceback.format_exc())
478 478 raise
479 479
480 480 def commit_change(self, repo, repo_name, cs, user, author, message,
481 481 content, f_path):
482 482 """
483 483 Commits changes
484 484
485 485 :param repo: SCM instance
486 486
487 487 """
488 488 user = self._get_user(user)
489 489 IMC = self._get_IMC_module(repo.alias)
490 490
491 491 # decoding here will force that we have proper encoded values
492 492 # in any other case this will throw exceptions and deny commit
493 493 content = safe_str(content)
494 494 path = safe_str(f_path)
495 495 # message and author needs to be unicode
496 496 # proper backend should then translate that into required type
497 497 message = safe_unicode(message)
498 498 author = safe_unicode(author)
499 m = IMC(repo)
500 m.change(FileNode(path, content))
501 tip = m.commit(message=message,
499 imc = IMC(repo)
500 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
501 tip = imc.commit(message=message,
502 502 author=author,
503 503 parents=[cs], branch=cs.branch)
504 504
505 505 self.mark_for_invalidation(repo_name)
506 506 self._handle_push(repo,
507 507 username=user.username,
508 508 action='push_local',
509 509 repo_name=repo_name,
510 510 revisions=[tip.raw_id])
511 511 return tip
512 512
513 513 def create_node(self, repo, repo_name, cs, user, author, message, content,
514 514 f_path):
515 515 user = self._get_user(user)
516 516 IMC = self._get_IMC_module(repo.alias)
517 517
518 518 # decoding here will force that we have proper encoded values
519 519 # in any other case this will throw exceptions and deny commit
520 520 if isinstance(content, (basestring,)):
521 521 content = safe_str(content)
522 522 elif isinstance(content, (file, cStringIO.OutputType,)):
523 523 content = content.read()
524 524 else:
525 525 raise Exception('Content is of unrecognized type %s' % (
526 526 type(content)
527 527 ))
528 528
529 529 message = safe_unicode(message)
530 530 author = safe_unicode(author)
531 531 path = safe_str(f_path)
532 532 m = IMC(repo)
533 533
534 534 if isinstance(cs, EmptyChangeset):
535 535 # EmptyChangeset means we we're editing empty repository
536 536 parents = None
537 537 else:
538 538 parents = [cs]
539 539
540 540 m.add(FileNode(path, content=content))
541 541 tip = m.commit(message=message,
542 542 author=author,
543 543 parents=parents, branch=cs.branch)
544 544
545 545 self.mark_for_invalidation(repo_name)
546 546 self._handle_push(repo,
547 547 username=user.username,
548 548 action='push_local',
549 549 repo_name=repo_name,
550 550 revisions=[tip.raw_id])
551 551 return tip
552 552
553 553 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
554 554 """
555 555 recursive walk in root dir and return a set of all path in that dir
556 556 based on repository walk function
557 557
558 558 :param repo_name: name of repository
559 559 :param revision: revision for which to list nodes
560 560 :param root_path: root path to list
561 561 :param flat: return as a list, if False returns a dict with decription
562 562
563 563 """
564 564 _files = list()
565 565 _dirs = list()
566 566 try:
567 567 _repo = self.__get_repo(repo_name)
568 568 changeset = _repo.scm_instance.get_changeset(revision)
569 569 root_path = root_path.lstrip('/')
570 570 for topnode, dirs, files in changeset.walk(root_path):
571 571 for f in files:
572 572 _files.append(f.path if flat else {"name": f.path,
573 573 "type": "file"})
574 574 for d in dirs:
575 575 _dirs.append(d.path if flat else {"name": d.path,
576 576 "type": "dir"})
577 577 except RepositoryError:
578 578 log.debug(traceback.format_exc())
579 579 raise
580 580
581 581 return _dirs, _files
582 582
583 583 def get_unread_journal(self):
584 584 return self.sa.query(UserLog).count()
585 585
586 586 def get_repo_landing_revs(self, repo=None):
587 587 """
588 588 Generates select option with tags branches and bookmarks (for hg only)
589 589 grouped by type
590 590
591 591 :param repo:
592 592 :type repo:
593 593 """
594 594
595 595 hist_l = []
596 596 choices = []
597 597 repo = self.__get_repo(repo)
598 598 hist_l.append(['tip', _('latest tip')])
599 599 choices.append('tip')
600 600 if not repo:
601 601 return choices, hist_l
602 602
603 603 repo = repo.scm_instance
604 604
605 605 branches_group = ([(k, k) for k, v in
606 606 repo.branches.iteritems()], _("Branches"))
607 607 hist_l.append(branches_group)
608 608 choices.extend([x[0] for x in branches_group[0]])
609 609
610 610 if repo.alias == 'hg':
611 611 bookmarks_group = ([(k, k) for k, v in
612 612 repo.bookmarks.iteritems()], _("Bookmarks"))
613 613 hist_l.append(bookmarks_group)
614 614 choices.extend([x[0] for x in bookmarks_group[0]])
615 615
616 616 tags_group = ([(k, k) for k, v in
617 617 repo.tags.iteritems()], _("Tags"))
618 618 hist_l.append(tags_group)
619 619 choices.extend([x[0] for x in tags_group[0]])
620 620
621 621 return choices, hist_l
622 622
623 623 def install_git_hook(self, repo, force_create=False):
624 624 """
625 625 Creates a rhodecode hook inside a git repository
626 626
627 627 :param repo: Instance of VCS repo
628 628 :param force_create: Create even if same name hook exists
629 629 """
630 630
631 631 loc = jn(repo.path, 'hooks')
632 632 if not repo.bare:
633 633 loc = jn(repo.path, '.git', 'hooks')
634 634 if not os.path.isdir(loc):
635 635 os.makedirs(loc)
636 636
637 637 tmpl_post = pkg_resources.resource_string(
638 638 'rhodecode', jn('config', 'post_receive_tmpl.py')
639 639 )
640 640 tmpl_pre = pkg_resources.resource_string(
641 641 'rhodecode', jn('config', 'pre_receive_tmpl.py')
642 642 )
643 643
644 644 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
645 645 _hook_file = jn(loc, '%s-receive' % h_type)
646 646 _rhodecode_hook = False
647 647 log.debug('Installing git hook in repo %s' % repo)
648 648 if os.path.exists(_hook_file):
649 649 # let's take a look at this hook, maybe it's rhodecode ?
650 650 log.debug('hook exists, checking if it is from rhodecode')
651 651 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
652 652 with open(_hook_file, 'rb') as f:
653 653 data = f.read()
654 654 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
655 655 % 'RC_HOOK_VER').search(data)
656 656 if matches:
657 657 try:
658 658 ver = matches.groups()[0]
659 659 log.debug('got %s it is rhodecode' % (ver))
660 660 _rhodecode_hook = True
661 661 except Exception:
662 662 log.error(traceback.format_exc())
663 663 else:
664 664 # there is no hook in this dir, so we want to create one
665 665 _rhodecode_hook = True
666 666
667 667 if _rhodecode_hook or force_create:
668 668 log.debug('writing %s hook file !' % h_type)
669 669 with open(_hook_file, 'wb') as f:
670 670 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
671 671 f.write(tmpl)
672 672 os.chmod(_hook_file, 0755)
673 673 else:
674 674 log.debug('skipping writing hook file')
General Comments 0
You need to be logged in to leave comments. Login now