##// END OF EJS Templates
various fixes for git and mercurial with InMemoryCommit backend and non-ascii files...
marcink -
r2199:31ebf701 beta
parent child Browse files
Show More
@@ -1,446 +1,447 b''
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 9 from rhodecode.lib.vcs.exceptions import VCSError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
14 14 from rhodecode.lib.vcs.utils import safe_unicode
15 15 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 16 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 17
18 18
19 19 class GitChangeset(BaseChangeset):
20 20 """
21 21 Represents state of the repository at single revision.
22 22 """
23 23
24 24 def __init__(self, repository, revision):
25 25 self._stat_modes = {}
26 26 self.repository = repository
27 27 self.raw_id = revision
28 28 self.revision = repository.revisions.index(revision)
29 29
30 30 self.short_id = self.raw_id[:12]
31 31 self.id = self.raw_id
32 32 try:
33 33 commit = self.repository._repo.get_object(self.raw_id)
34 34 except KeyError:
35 35 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 36 self._commit = commit
37 37 self._tree_id = commit.tree
38 38
39 39 try:
40 40 self.message = safe_unicode(commit.message[:-1])
41 41 # Always strip last eol
42 42 except UnicodeDecodeError:
43 43 self.message = commit.message[:-1].decode(commit.encoding
44 44 or 'utf-8')
45 45 #self.branch = None
46 46 self.tags = []
47 47 #tree = self.repository.get_object(self._tree_id)
48 48 self.nodes = {}
49 49 self._paths = {}
50 50
51 51 @LazyProperty
52 52 def author(self):
53 53 return safe_unicode(self._commit.committer)
54 54
55 55 @LazyProperty
56 56 def date(self):
57 57 return date_fromtimestamp(self._commit.commit_time,
58 58 self._commit.commit_timezone)
59 59
60 60 @LazyProperty
61 61 def status(self):
62 62 """
63 63 Returns modified, added, removed, deleted files for current changeset
64 64 """
65 65 return self.changed, self.added, self.removed
66 66
67 67 @LazyProperty
68 68 def branch(self):
69 69
70 70 heads = self.repository._heads(reverse=False)
71 71
72 72 ref = heads.get(self.raw_id)
73 73 if ref:
74 74 return safe_unicode(ref)
75 75
76
77 76 def _fix_path(self, path):
78 77 """
79 78 Paths are stored without trailing slash so we need to get rid off it if
80 79 needed.
81 80 """
82 81 if path.endswith('/'):
83 82 path = path.rstrip('/')
84 83 return path
85 84
86 85 def _get_id_for_path(self, path):
87 86
88 87 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
89 88 if not path in self._paths:
90 89 path = path.strip('/')
91 90 # set root tree
92 91 tree = self.repository._repo[self._commit.tree]
93 92 if path == '':
94 93 self._paths[''] = tree.id
95 94 return tree.id
96 95 splitted = path.split('/')
97 96 dirs, name = splitted[:-1], splitted[-1]
98 97 curdir = ''
99 98
100 99 # initially extract things from root dir
101 100 for item, stat, id in tree.iteritems():
102 101 if curdir:
103 102 name = '/'.join((curdir, item))
104 103 else:
105 104 name = item
106 105 self._paths[name] = id
107 106 self._stat_modes[name] = stat
108 107
109 108 for dir in dirs:
110 109 if curdir:
111 110 curdir = '/'.join((curdir, dir))
112 111 else:
113 112 curdir = dir
114 113 dir_id = None
115 114 for item, stat, id in tree.iteritems():
116 115 if dir == item:
117 116 dir_id = id
118 117 if dir_id:
119 118 # Update tree
120 119 tree = self.repository._repo[dir_id]
121 120 if not isinstance(tree, objects.Tree):
122 121 raise ChangesetError('%s is not a directory' % curdir)
123 122 else:
124 123 raise ChangesetError('%s have not been found' % curdir)
125 124
126 125 # cache all items from the given traversed tree
127 126 for item, stat, id in tree.iteritems():
128 127 if curdir:
129 128 name = '/'.join((curdir, item))
130 129 else:
131 130 name = item
132 131 self._paths[name] = id
133 132 self._stat_modes[name] = stat
134
135 133 if not path in self._paths:
136 134 raise NodeDoesNotExistError("There is no file nor directory "
137 135 "at the given path %r at revision %r"
138 136 % (path, self.short_id))
139 137 return self._paths[path]
140 138
141 139 def _get_kind(self, path):
142 140 id = self._get_id_for_path(path)
143 141 obj = self.repository._repo[id]
144 142 if isinstance(obj, objects.Blob):
145 143 return NodeKind.FILE
146 144 elif isinstance(obj, objects.Tree):
147 145 return NodeKind.DIR
148 146
149 147 def _get_file_nodes(self):
150 148 return chain(*(t[2] for t in self.walk()))
151 149
152 150 @LazyProperty
153 151 def parents(self):
154 152 """
155 153 Returns list of parents changesets.
156 154 """
157 155 return [self.repository.get_changeset(parent)
158 156 for parent in self._commit.parents]
159 157
160 158 def next(self, branch=None):
161 159
162 160 if branch and self.branch != branch:
163 161 raise VCSError('Branch option used on changeset not belonging '
164 162 'to that branch')
165 163
166 164 def _next(changeset, branch):
167 165 try:
168 166 next_ = changeset.revision + 1
169 167 next_rev = changeset.repository.revisions[next_]
170 168 except IndexError:
171 169 raise ChangesetDoesNotExistError
172 170 cs = changeset.repository.get_changeset(next_rev)
173 171
174 172 if branch and branch != cs.branch:
175 173 return _next(cs, branch)
176 174
177 175 return cs
178 176
179 177 return _next(self, branch)
180 178
181 179 def prev(self, branch=None):
182 180 if branch and self.branch != branch:
183 181 raise VCSError('Branch option used on changeset not belonging '
184 182 'to that branch')
185 183
186 184 def _prev(changeset, branch):
187 185 try:
188 186 prev_ = changeset.revision - 1
189 187 if prev_ < 0:
190 188 raise IndexError
191 189 prev_rev = changeset.repository.revisions[prev_]
192 190 except IndexError:
193 191 raise ChangesetDoesNotExistError
194 192
195 193 cs = changeset.repository.get_changeset(prev_rev)
196 194
197 195 if branch and branch != cs.branch:
198 196 return _prev(cs, branch)
199 197
200 198 return cs
201 199
202 200 return _prev(self, branch)
203 201
204 202 def get_file_mode(self, path):
205 203 """
206 204 Returns stat mode of the file at the given ``path``.
207 205 """
208 206 # ensure path is traversed
209 207 self._get_id_for_path(path)
210 208 return self._stat_modes[path]
211 209
212 210 def get_file_content(self, path):
213 211 """
214 212 Returns content of the file at given ``path``.
215 213 """
216 214 id = self._get_id_for_path(path)
217 215 blob = self.repository._repo[id]
218 216 return blob.as_pretty_string()
219 217
220 218 def get_file_size(self, path):
221 219 """
222 220 Returns size of the file at given ``path``.
223 221 """
224 222 id = self._get_id_for_path(path)
225 223 blob = self.repository._repo[id]
226 224 return blob.raw_length()
227 225
228 226 def get_file_changeset(self, path):
229 227 """
230 228 Returns last commit of the file at the given ``path``.
231 229 """
232 230 node = self.get_node(path)
233 231 return node.history[0]
234 232
235 233 def get_file_history(self, path):
236 234 """
237 235 Returns history of file as reversed list of ``Changeset`` objects for
238 236 which file at given ``path`` has been modified.
239 237
240 238 TODO: This function now uses os underlying 'git' and 'grep' commands
241 239 which is generally not good. Should be replaced with algorithm
242 240 iterating commits.
243 241 """
244 242 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
245 243 self.id, path
246 244 )
247 245 so, se = self.repository.run_git_command(cmd)
248 246 ids = re.findall(r'\w{40}', so)
249 247 return [self.repository.get_changeset(id) for id in ids]
250 248
251 249 def get_file_annotate(self, path):
252 250 """
253 251 Returns a list of three element tuples with lineno,changeset and line
254 252
255 253 TODO: This function now uses os underlying 'git' command which is
256 254 generally not good. Should be replaced with algorithm iterating
257 255 commits.
258 256 """
259 257 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
260 258 # -l ==> outputs long shas (and we need all 40 characters)
261 259 # --root ==> doesn't put '^' character for bounderies
262 260 # -r sha ==> blames for the given revision
263 261 so, se = self.repository.run_git_command(cmd)
264 262 annotate = []
265 263 for i, blame_line in enumerate(so.split('\n')[:-1]):
266 264 ln_no = i + 1
267 265 id, line = re.split(r' \(.+?\) ', blame_line, 1)
268 266 annotate.append((ln_no, self.repository.get_changeset(id), line))
269 267 return annotate
270 268
271 269 def fill_archive(self, stream=None, kind='tgz', prefix=None,
272 270 subrepos=False):
273 271 """
274 272 Fills up given stream.
275 273
276 274 :param stream: file like object.
277 275 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
278 276 Default: ``tgz``.
279 277 :param prefix: name of root directory in archive.
280 278 Default is repository name and changeset's raw_id joined with dash
281 279 (``repo-tip.<KIND>``).
282 280 :param subrepos: include subrepos in this archive.
283 281
284 282 :raise ImproperArchiveTypeError: If given kind is wrong.
285 283 :raise VcsError: If given stream is None
286 284
287 285 """
288 286 allowed_kinds = settings.ARCHIVE_SPECS.keys()
289 287 if kind not in allowed_kinds:
290 288 raise ImproperArchiveTypeError('Archive kind not supported use one'
291 289 'of %s', allowed_kinds)
292 290
293 291 if prefix is None:
294 292 prefix = '%s-%s' % (self.repository.name, self.short_id)
295 293 elif prefix.startswith('/'):
296 294 raise VCSError("Prefix cannot start with leading slash")
297 295 elif prefix.strip() == '':
298 296 raise VCSError("Prefix cannot be empty")
299 297
300 298 if kind == 'zip':
301 299 frmt = 'zip'
302 300 else:
303 301 frmt = 'tar'
304 302 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
305 303 self.raw_id)
306 304 if kind == 'tgz':
307 305 cmd += ' | gzip -9'
308 306 elif kind == 'tbz2':
309 307 cmd += ' | bzip2 -9'
310 308
311 309 if stream is None:
312 310 raise VCSError('You need to pass in a valid stream for filling'
313 311 ' with archival data')
314 312 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
315 313 cwd=self.repository.path)
316 314
317 315 buffer_size = 1024 * 8
318 316 chunk = popen.stdout.read(buffer_size)
319 317 while chunk:
320 318 stream.write(chunk)
321 319 chunk = popen.stdout.read(buffer_size)
322 320 # Make sure all descriptors would be read
323 321 popen.communicate()
324 322
325 323 def get_nodes(self, path):
326 324 if self._get_kind(path) != NodeKind.DIR:
327 325 raise ChangesetError("Directory does not exist for revision %r at "
328 326 " %r" % (self.revision, path))
329 327 path = self._fix_path(path)
330 328 id = self._get_id_for_path(path)
331 329 tree = self.repository._repo[id]
332 330 dirnodes = []
333 331 filenodes = []
334 332 for name, stat, id in tree.iteritems():
335 333 obj = self.repository._repo.get_object(id)
336 334 if path != '':
337 335 obj_path = '/'.join((path, name))
338 336 else:
339 337 obj_path = name
340 338 if obj_path not in self._stat_modes:
341 339 self._stat_modes[obj_path] = stat
342 340 if isinstance(obj, objects.Tree):
343 341 dirnodes.append(DirNode(obj_path, changeset=self))
344 342 elif isinstance(obj, objects.Blob):
345 343 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
346 344 else:
347 345 raise ChangesetError("Requested object should be Tree "
348 346 "or Blob, is %r" % type(obj))
349 347 nodes = dirnodes + filenodes
350 348 for node in nodes:
351 349 if not node.path in self.nodes:
352 350 self.nodes[node.path] = node
353 351 nodes.sort()
354 352 return nodes
355 353
356 354 def get_node(self, path):
357 355 if isinstance(path, unicode):
358 356 path = path.encode('utf-8')
359 357 path = self._fix_path(path)
360 358 if not path in self.nodes:
361 359 try:
362 360 id = self._get_id_for_path(path)
363 361 except ChangesetError:
364 362 raise NodeDoesNotExistError("Cannot find one of parents' "
365 363 "directories for a given path: %s" % path)
366 364 obj = self.repository._repo.get_object(id)
367 365 if isinstance(obj, objects.Tree):
368 366 if path == '':
369 367 node = RootNode(changeset=self)
370 368 else:
371 369 node = DirNode(path, changeset=self)
372 370 node._tree = obj
373 371 elif isinstance(obj, objects.Blob):
374 372 node = FileNode(path, changeset=self)
375 373 node._blob = obj
376 374 else:
377 375 raise NodeDoesNotExistError("There is no file nor directory "
378 376 "at the given path %r at revision %r"
379 377 % (path, self.short_id))
380 378 # cache node
381 379 self.nodes[path] = node
382 380 return self.nodes[path]
383 381
384 382 @LazyProperty
385 383 def affected_files(self):
386 384 """
387 385 Get's a fast accessible file changes for given changeset
388 386 """
389 387
390 388 return self.added + self.changed
391 389
392 390 @LazyProperty
393 391 def _diff_name_status(self):
394 392 output = []
395 393 for parent in self.parents:
396 cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
394 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
397 395 so, se = self.repository.run_git_command(cmd)
398 396 output.append(so.strip())
399 397 return '\n'.join(output)
400 398
401 399 def _get_paths_for_status(self, status):
402 400 """
403 401 Returns sorted list of paths for given ``status``.
404 402
405 403 :param status: one of: *added*, *modified* or *deleted*
406 404 """
407 405 paths = set()
408 406 char = status[0].upper()
409 407 for line in self._diff_name_status.splitlines():
410 408 if not line:
411 409 continue
410
412 411 if line.startswith(char):
413 412 splitted = line.split(char,1)
414 413 if not len(splitted) == 2:
415 414 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
416 415 "particularly that line: %s" % (self._diff_name_status,
417 416 line))
418 paths.add(splitted[1].strip())
417 _path = splitted[1].strip()
418 paths.add(_path)
419
419 420 return sorted(paths)
420 421
421 422 @LazyProperty
422 423 def added(self):
423 424 """
424 425 Returns list of added ``FileNode`` objects.
425 426 """
426 427 if not self.parents:
427 428 return list(self._get_file_nodes())
428 429 return [self.get_node(path) for path in self._get_paths_for_status('added')]
429 430
430 431 @LazyProperty
431 432 def changed(self):
432 433 """
433 434 Returns list of modified ``FileNode`` objects.
434 435 """
435 436 if not self.parents:
436 437 return []
437 438 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
438 439
439 440 @LazyProperty
440 441 def removed(self):
441 442 """
442 443 Returns list of removed ``FileNode`` objects.
443 444 """
444 445 if not self.parents:
445 446 return []
446 447 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,192 +1,193 b''
1 1 import time
2 2 import datetime
3 3 import posixpath
4 4 from dulwich import objects
5 5 from dulwich.repo import Repo
6 6 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
7 7 from rhodecode.lib.vcs.exceptions import RepositoryError
8 from rhodecode.lib.vcs.utils import safe_str
8 9
9 10
10 11 class GitInMemoryChangeset(BaseInMemoryChangeset):
11 12
12 13 def commit(self, message, author, parents=None, branch=None, date=None,
13 14 **kwargs):
14 15 """
15 16 Performs in-memory commit (doesn't check workdir in any way) and
16 17 returns newly created ``Changeset``. Updates repository's
17 18 ``revisions``.
18 19
19 20 :param message: message of the commit
20 21 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 22 :param parents: single parent or sequence of parents from which commit
22 23 would be derieved
23 24 :param date: ``datetime.datetime`` instance. Defaults to
24 25 ``datetime.datetime.now()``.
25 26 :param branch: branch name, as string. If none given, default backend's
26 27 branch would be used.
27 28
28 29 :raises ``CommitError``: if any error occurs while committing
29 30 """
30 31 self.check_integrity(parents)
31 32
32 33 from .repository import GitRepository
33 34 if branch is None:
34 35 branch = GitRepository.DEFAULT_BRANCH_NAME
35 36
36 37 repo = self.repository._repo
37 38 object_store = repo.object_store
38 39
39 40 ENCODING = "UTF-8"
40 41 DIRMOD = 040000
41 42
42 43 # Create tree and populates it with blobs
43 44 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or\
44 45 objects.Tree()
45 46 for node in self.added + self.changed:
46 47 # Compute subdirs if needed
47 48 dirpath, nodename = posixpath.split(node.path)
48 49 dirnames = dirpath and dirpath.split('/') or []
49 50 parent = commit_tree
50 51 ancestors = [('', parent)]
51 52
52 53 # Tries to dig for the deepest existing tree
53 54 while dirnames:
54 55 curdir = dirnames.pop(0)
55 56 try:
56 57 dir_id = parent[curdir][1]
57 58 except KeyError:
58 59 # put curdir back into dirnames and stops
59 60 dirnames.insert(0, curdir)
60 61 break
61 62 else:
62 63 # If found, updates parent
63 64 parent = self.repository._repo[dir_id]
64 65 ancestors.append((curdir, parent))
65 66 # Now parent is deepest exising tree and we need to create subtrees
66 67 # for dirnames (in reverse order) [this only applies for nodes from added]
67 68 new_trees = []
68 69 blob = objects.Blob.from_string(node.content.encode(ENCODING))
69 70 node_path = node.name.encode(ENCODING)
70 71 if dirnames:
71 72 # If there are trees which should be created we need to build
72 73 # them now (in reverse order)
73 74 reversed_dirnames = list(reversed(dirnames))
74 75 curtree = objects.Tree()
75 76 curtree[node_path] = node.mode, blob.id
76 77 new_trees.append(curtree)
77 78 for dirname in reversed_dirnames[:-1]:
78 79 newtree = objects.Tree()
79 80 #newtree.add(DIRMOD, dirname, curtree.id)
80 81 newtree[dirname] = DIRMOD, curtree.id
81 82 new_trees.append(newtree)
82 83 curtree = newtree
83 84 parent[reversed_dirnames[-1]] = DIRMOD, curtree.id
84 85 else:
85 86 parent.add(node.mode, node_path, blob.id)
86 87 new_trees.append(parent)
87 88 # Update ancestors
88 89 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
89 90 zip(ancestors, ancestors[1:])]):
90 91 parent[path] = DIRMOD, tree.id
91 92 object_store.add_object(tree)
92 93
93 94 object_store.add_object(blob)
94 95 for tree in new_trees:
95 96 object_store.add_object(tree)
96 97 for node in self.removed:
97 98 paths = node.path.split('/')
98 99 tree = commit_tree
99 100 trees = [tree]
100 101 # Traverse deep into the forest...
101 102 for path in paths:
102 103 try:
103 104 obj = self.repository._repo[tree[path][1]]
104 105 if isinstance(obj, objects.Tree):
105 106 trees.append(obj)
106 107 tree = obj
107 108 except KeyError:
108 109 break
109 110 # Cut down the blob and all rotten trees on the way back...
110 111 for path, tree in reversed(zip(paths, trees)):
111 112 del tree[path]
112 113 if tree:
113 114 # This tree still has elements - don't remove it or any
114 115 # of it's parents
115 116 break
116 117
117 118 object_store.add_object(commit_tree)
118 119
119 120 # Create commit
120 121 commit = objects.Commit()
121 122 commit.tree = commit_tree.id
122 123 commit.parents = [p._commit.id for p in self.parents if p]
123 commit.author = commit.committer = author
124 commit.author = commit.committer = safe_str(author)
124 125 commit.encoding = ENCODING
125 commit.message = message + ' '
126 commit.message = safe_str(message) + ' '
126 127
127 128 # Compute date
128 129 if date is None:
129 130 date = time.time()
130 131 elif isinstance(date, datetime.datetime):
131 132 date = time.mktime(date.timetuple())
132 133
133 134 author_time = kwargs.pop('author_time', date)
134 135 commit.commit_time = int(date)
135 136 commit.author_time = int(author_time)
136 137 tz = time.timezone
137 138 author_tz = kwargs.pop('author_timezone', tz)
138 139 commit.commit_timezone = tz
139 140 commit.author_timezone = author_tz
140 141
141 142 object_store.add_object(commit)
142 143
143 144 ref = 'refs/heads/%s' % branch
144 145 repo.refs[ref] = commit.id
145 146 repo.refs.set_symbolic_ref('HEAD', ref)
146 147
147 148 # Update vcs repository object & recreate dulwich repo
148 149 self.repository.revisions.append(commit.id)
149 150 self.repository._repo = Repo(self.repository.path)
150 151 tip = self.repository.get_changeset()
151 152 self.reset()
152 153 return tip
153 154
154 155 def _get_missing_trees(self, path, root_tree):
155 156 """
156 157 Creates missing ``Tree`` objects for the given path.
157 158
158 159 :param path: path given as a string. It may be a path to a file node
159 160 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
160 161 end with slash (i.e. ``foo/bar/``).
161 162 :param root_tree: ``dulwich.objects.Tree`` object from which we start
162 163 traversing (should be commit's root tree)
163 164 """
164 165 dirpath = posixpath.split(path)[0]
165 166 dirs = dirpath.split('/')
166 167 if not dirs or dirs == ['']:
167 168 return []
168 169
169 170 def get_tree_for_dir(tree, dirname):
170 171 for name, mode, id in tree.iteritems():
171 172 if name == dirname:
172 173 obj = self.repository._repo[id]
173 174 if isinstance(obj, objects.Tree):
174 175 return obj
175 176 else:
176 177 raise RepositoryError("Cannot create directory %s "
177 178 "at tree %s as path is occupied and is not a "
178 179 "Tree" % (dirname, tree))
179 180 return None
180 181
181 182 trees = []
182 183 parent = root_tree
183 184 for dirname in dirs:
184 185 tree = get_tree_for_dir(parent, dirname)
185 186 if tree is None:
186 187 tree = objects.Tree()
187 188 dirmode = 040000
188 189 parent.add(dirmode, dirname, tree.id)
189 190 parent = tree
190 191 # Always append tree
191 192 trees.append(tree)
192 193 return trees
@@ -1,521 +1,523 b''
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 from dulwich.repo import Repo, NotGitRepository
17 17 #from dulwich.config import ConfigFile
18 18 from string import Template
19 19 from subprocess import Popen, PIPE
20 20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 30 from rhodecode.lib.vcs.utils.paths import abspath
31 31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 32 from .workdir import GitWorkdir
33 33 from .changeset import GitChangeset
34 34 from .inmemory import GitInMemoryChangeset
35 35 from .config import ConfigFile
36 36
37 37
38 38 class GitRepository(BaseRepository):
39 39 """
40 40 Git repository backend.
41 41 """
42 42 DEFAULT_BRANCH_NAME = 'master'
43 43 scm = 'git'
44 44
45 45 def __init__(self, repo_path, create=False, src_url=None,
46 46 update_after_clone=False, bare=False):
47 47
48 48 self.path = abspath(repo_path)
49 49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 50 try:
51 51 self.head = self._repo.head()
52 52 except KeyError:
53 53 self.head = None
54 54
55 55 self._config_files = [
56 56 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
57 57 'config'),
58 58 abspath(get_user_home(), '.gitconfig'),
59 59 ]
60 60
61 61 @LazyProperty
62 62 def revisions(self):
63 63 """
64 64 Returns list of revisions' ids, in ascending order. Being lazy
65 65 attribute allows external tools to inject shas from cache.
66 66 """
67 67 return self._get_all_revisions()
68 68
69 69 def run_git_command(self, cmd):
70 70 """
71 71 Runs given ``cmd`` as git command and returns tuple
72 72 (returncode, stdout, stderr).
73 73
74 74 .. note::
75 75 This method exists only until log/blame functionality is implemented
76 76 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
77 77 os command's output is road to hell...
78 78
79 79 :param cmd: git command to be executed
80 80 """
81 81
82 #cmd = '(cd %s && git %s)' % (self.path, cmd)
82 _copts = ['-c', 'core.quotepath=false', ]
83
83 84 if isinstance(cmd, basestring):
84 cmd = 'GIT_CONFIG_NOGLOBAL=1 git %s' % cmd
85 else:
86 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + cmd
85 cmd = [cmd]
86
87 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
88
87 89 try:
88 90 opts = dict(
89 91 shell=isinstance(cmd, basestring),
90 92 stdout=PIPE,
91 93 stderr=PIPE)
92 94 if os.path.isdir(self.path):
93 95 opts['cwd'] = self.path
94 96 p = Popen(cmd, **opts)
95 97 except OSError, err:
96 98 raise RepositoryError("Couldn't run git command (%s).\n"
97 99 "Original error was:%s" % (cmd, err))
98 100 so, se = p.communicate()
99 101 if not se.startswith("fatal: bad default revision 'HEAD'") and \
100 102 p.returncode != 0:
101 103 raise RepositoryError("Couldn't run git command (%s).\n"
102 104 "stderr:\n%s" % (cmd, se))
103 105 return so, se
104 106
105 107 def _check_url(self, url):
106 108 """
107 109 Functon will check given url and try to verify if it's a valid
108 110 link. Sometimes it may happened that mercurial will issue basic
109 111 auth request that can cause whole API to hang when used from python
110 112 or other external calls.
111 113
112 114 On failures it'll raise urllib2.HTTPError
113 115 """
114 116
115 117 #TODO: implement this
116 118 pass
117 119
118 120 def _get_repo(self, create, src_url=None, update_after_clone=False,
119 121 bare=False):
120 122 if create and os.path.exists(self.path):
121 123 raise RepositoryError("Location already exist")
122 124 if src_url and not create:
123 125 raise RepositoryError("Create should be set to True if src_url is "
124 126 "given (clone operation creates repository)")
125 127 try:
126 128 if create and src_url:
127 129 self._check_url(src_url)
128 130 self.clone(src_url, update_after_clone, bare)
129 131 return Repo(self.path)
130 132 elif create:
131 133 os.mkdir(self.path)
132 134 if bare:
133 135 return Repo.init_bare(self.path)
134 136 else:
135 137 return Repo.init(self.path)
136 138 else:
137 139 return Repo(self.path)
138 140 except (NotGitRepository, OSError), err:
139 141 raise RepositoryError(err)
140 142
141 143 def _get_all_revisions(self):
142 144 cmd = 'rev-list --all --date-order'
143 145 try:
144 146 so, se = self.run_git_command(cmd)
145 147 except RepositoryError:
146 148 # Can be raised for empty repositories
147 149 return []
148 150 revisions = so.splitlines()
149 151 revisions.reverse()
150 152 return revisions
151 153
152 154 def _get_revision(self, revision):
153 155 """
154 156 For git backend we always return integer here. This way we ensure
155 157 that changset's revision attribute would become integer.
156 158 """
157 159 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
158 160 is_bstr = lambda o: isinstance(o, (str, unicode))
159 161 is_null = lambda o: len(o) == revision.count('0')
160 162
161 163 if len(self.revisions) == 0:
162 164 raise EmptyRepositoryError("There are no changesets yet")
163 165
164 166 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
165 167 revision = self.revisions[-1]
166 168
167 169 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
168 170 or isinstance(revision, int) or is_null(revision)):
169 171 try:
170 172 revision = self.revisions[int(revision)]
171 173 except:
172 174 raise ChangesetDoesNotExistError("Revision %r does not exist "
173 175 "for this repository %s" % (revision, self))
174 176
175 177 elif is_bstr(revision):
176 178 if not pattern.match(revision) or revision not in self.revisions:
177 179 raise ChangesetDoesNotExistError("Revision %r does not exist "
178 180 "for this repository %s" % (revision, self))
179 181
180 182 # Ensure we return full id
181 183 if not pattern.match(str(revision)):
182 184 raise ChangesetDoesNotExistError("Given revision %r not recognized"
183 185 % revision)
184 186 return revision
185 187
186 188 def _get_archives(self, archive_name='tip'):
187 189
188 190 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
189 191 yield {"type": i[0], "extension": i[1], "node": archive_name}
190 192
191 193 def _get_url(self, url):
192 194 """
193 195 Returns normalized url. If schema is not given, would fall to
194 196 filesystem (``file:///``) schema.
195 197 """
196 198 url = str(url)
197 199 if url != 'default' and not '://' in url:
198 200 url = ':///'.join(('file', url))
199 201 return url
200 202
201 203 @LazyProperty
202 204 def name(self):
203 205 return os.path.basename(self.path)
204 206
205 207 @LazyProperty
206 208 def last_change(self):
207 209 """
208 210 Returns last change made on this repository as datetime object
209 211 """
210 212 return date_fromtimestamp(self._get_mtime(), makedate()[1])
211 213
212 214 def _get_mtime(self):
213 215 try:
214 216 return time.mktime(self.get_changeset().date.timetuple())
215 217 except RepositoryError:
216 218 # fallback to filesystem
217 219 in_path = os.path.join(self.path, '.git', "index")
218 220 he_path = os.path.join(self.path, '.git', "HEAD")
219 221 if os.path.exists(in_path):
220 222 return os.stat(in_path).st_mtime
221 223 else:
222 224 return os.stat(he_path).st_mtime
223 225
224 226 @LazyProperty
225 227 def description(self):
226 228 undefined_description = u'unknown'
227 229 description_path = os.path.join(self.path, '.git', 'description')
228 230 if os.path.isfile(description_path):
229 231 return safe_unicode(open(description_path).read())
230 232 else:
231 233 return undefined_description
232 234
233 235 @LazyProperty
234 236 def contact(self):
235 237 undefined_contact = u'Unknown'
236 238 return undefined_contact
237 239
238 240 @property
239 241 def branches(self):
240 242 if not self.revisions:
241 243 return {}
242 244 refs = self._repo.refs.as_dict()
243 245 sortkey = lambda ctx: ctx[0]
244 246 _branches = [('/'.join(ref.split('/')[2:]), head)
245 247 for ref, head in refs.items()
246 248 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
247 249 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
248 250
249 251 def _heads(self, reverse=False):
250 252 refs = self._repo.get_refs()
251 253 heads = {}
252 254
253 255 for key, val in refs.items():
254 256 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
255 257 if key.startswith(ref_key):
256 258 n = key[len(ref_key):]
257 259 if n not in ['HEAD']:
258 260 heads[n] = val
259 261
260 262 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
261 263
262 264 def _get_tags(self):
263 265 if not self.revisions:
264 266 return {}
265 267 sortkey = lambda ctx: ctx[0]
266 268 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
267 269 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
268 270 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
269 271
270 272 @LazyProperty
271 273 def tags(self):
272 274 return self._get_tags()
273 275
274 276 def tag(self, name, user, revision=None, message=None, date=None,
275 277 **kwargs):
276 278 """
277 279 Creates and returns a tag for the given ``revision``.
278 280
279 281 :param name: name for new tag
280 282 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
281 283 :param revision: changeset id for which new tag would be created
282 284 :param message: message of the tag's commit
283 285 :param date: date of tag's commit
284 286
285 287 :raises TagAlreadyExistError: if tag with same name already exists
286 288 """
287 289 if name in self.tags:
288 290 raise TagAlreadyExistError("Tag %s already exists" % name)
289 291 changeset = self.get_changeset(revision)
290 292 message = message or "Added tag %s for commit %s" % (name,
291 293 changeset.raw_id)
292 294 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
293 295
294 296 self.tags = self._get_tags()
295 297 return changeset
296 298
297 299 def remove_tag(self, name, user, message=None, date=None):
298 300 """
299 301 Removes tag with the given ``name``.
300 302
301 303 :param name: name of the tag to be removed
302 304 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
303 305 :param message: message of the tag's removal commit
304 306 :param date: date of tag's removal commit
305 307
306 308 :raises TagDoesNotExistError: if tag with given name does not exists
307 309 """
308 310 if name not in self.tags:
309 311 raise TagDoesNotExistError("Tag %s does not exist" % name)
310 312 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
311 313 try:
312 314 os.remove(tagpath)
313 315 self.tags = self._get_tags()
314 316 except OSError, e:
315 317 raise RepositoryError(e.strerror)
316 318
317 319 def get_changeset(self, revision=None):
318 320 """
319 321 Returns ``GitChangeset`` object representing commit from git repository
320 322 at the given revision or head (most recent commit) if None given.
321 323 """
322 324 if isinstance(revision, GitChangeset):
323 325 return revision
324 326 revision = self._get_revision(revision)
325 327 changeset = GitChangeset(repository=self, revision=revision)
326 328 return changeset
327 329
328 330 def get_changesets(self, start=None, end=None, start_date=None,
329 331 end_date=None, branch_name=None, reverse=False):
330 332 """
331 333 Returns iterator of ``GitChangeset`` objects from start to end (both
332 334 are inclusive), in ascending date order (unless ``reverse`` is set).
333 335
334 336 :param start: changeset ID, as str; first returned changeset
335 337 :param end: changeset ID, as str; last returned changeset
336 338 :param start_date: if specified, changesets with commit date less than
337 339 ``start_date`` would be filtered out from returned set
338 340 :param end_date: if specified, changesets with commit date greater than
339 341 ``end_date`` would be filtered out from returned set
340 342 :param branch_name: if specified, changesets not reachable from given
341 343 branch would be filtered out from returned set
342 344 :param reverse: if ``True``, returned generator would be reversed
343 345 (meaning that returned changesets would have descending date order)
344 346
345 347 :raise BranchDoesNotExistError: If given ``branch_name`` does not
346 348 exist.
347 349 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
348 350 ``end`` could not be found.
349 351
350 352 """
351 353 if branch_name and branch_name not in self.branches:
352 354 raise BranchDoesNotExistError("Branch '%s' not found" \
353 355 % branch_name)
354 356 # %H at format means (full) commit hash, initial hashes are retrieved
355 357 # in ascending date order
356 358 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
357 359 cmd_params = {}
358 360 if start_date:
359 361 cmd_template += ' --since "$since"'
360 362 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
361 363 if end_date:
362 364 cmd_template += ' --until "$until"'
363 365 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
364 366 if branch_name:
365 367 cmd_template += ' $branch_name'
366 368 cmd_params['branch_name'] = branch_name
367 369 else:
368 370 cmd_template += ' --all'
369 371
370 372 cmd = Template(cmd_template).safe_substitute(**cmd_params)
371 373 revs = self.run_git_command(cmd)[0].splitlines()
372 374 start_pos = 0
373 375 end_pos = len(revs)
374 376 if start:
375 377 _start = self._get_revision(start)
376 378 try:
377 379 start_pos = revs.index(_start)
378 380 except ValueError:
379 381 pass
380 382
381 383 if end is not None:
382 384 _end = self._get_revision(end)
383 385 try:
384 386 end_pos = revs.index(_end)
385 387 except ValueError:
386 388 pass
387 389
388 390 if None not in [start, end] and start_pos > end_pos:
389 391 raise RepositoryError('start cannot be after end')
390 392
391 393 if end_pos is not None:
392 394 end_pos += 1
393 395
394 396 revs = revs[start_pos:end_pos]
395 397 if reverse:
396 398 revs = reversed(revs)
397 399 for rev in revs:
398 400 yield self.get_changeset(rev)
399 401
400 402 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
401 403 context=3):
402 404 """
403 405 Returns (git like) *diff*, as plain text. Shows changes introduced by
404 406 ``rev2`` since ``rev1``.
405 407
406 408 :param rev1: Entry point from which diff is shown. Can be
407 409 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
408 410 the changes since empty state of the repository until ``rev2``
409 411 :param rev2: Until which revision changes should be shown.
410 412 :param ignore_whitespace: If set to ``True``, would not show whitespace
411 413 changes. Defaults to ``False``.
412 414 :param context: How many lines before/after changed lines should be
413 415 shown. Defaults to ``3``.
414 416 """
415 417 flags = ['-U%s' % context]
416 418 if ignore_whitespace:
417 419 flags.append('-w')
418 420
419 421 if rev1 == self.EMPTY_CHANGESET:
420 422 rev2 = self.get_changeset(rev2).raw_id
421 423 cmd = ' '.join(['show'] + flags + [rev2])
422 424 else:
423 425 rev1 = self.get_changeset(rev1).raw_id
424 426 rev2 = self.get_changeset(rev2).raw_id
425 427 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
426 428
427 429 if path:
428 430 cmd += ' -- "%s"' % path
429 431 stdout, stderr = self.run_git_command(cmd)
430 432 # If we used 'show' command, strip first few lines (until actual diff
431 433 # starts)
432 434 if rev1 == self.EMPTY_CHANGESET:
433 435 lines = stdout.splitlines()
434 436 x = 0
435 437 for line in lines:
436 438 if line.startswith('diff'):
437 439 break
438 440 x += 1
439 441 # Append new line just like 'diff' command do
440 442 stdout = '\n'.join(lines[x:]) + '\n'
441 443 return stdout
442 444
443 445 @LazyProperty
444 446 def in_memory_changeset(self):
445 447 """
446 448 Returns ``GitInMemoryChangeset`` object for this repository.
447 449 """
448 450 return GitInMemoryChangeset(self)
449 451
450 452 def clone(self, url, update_after_clone=True, bare=False):
451 453 """
452 454 Tries to clone changes from external location.
453 455
454 456 :param update_after_clone: If set to ``False``, git won't checkout
455 457 working directory
456 458 :param bare: If set to ``True``, repository would be cloned into
457 459 *bare* git repository (no working directory at all).
458 460 """
459 461 url = self._get_url(url)
460 462 cmd = ['clone']
461 463 if bare:
462 464 cmd.append('--bare')
463 465 elif not update_after_clone:
464 466 cmd.append('--no-checkout')
465 467 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
466 468 cmd = ' '.join(cmd)
467 469 # If error occurs run_git_command raises RepositoryError already
468 470 self.run_git_command(cmd)
469 471
470 472 @LazyProperty
471 473 def workdir(self):
472 474 """
473 475 Returns ``Workdir`` instance for this repository.
474 476 """
475 477 return GitWorkdir(self)
476 478
477 479 def get_config_value(self, section, name, config_file=None):
478 480 """
479 481 Returns configuration value for a given [``section``] and ``name``.
480 482
481 483 :param section: Section we want to retrieve value from
482 484 :param name: Name of configuration we want to retrieve
483 485 :param config_file: A path to file which should be used to retrieve
484 486 configuration from (might also be a list of file paths)
485 487 """
486 488 if config_file is None:
487 489 config_file = []
488 490 elif isinstance(config_file, basestring):
489 491 config_file = [config_file]
490 492
491 493 def gen_configs():
492 494 for path in config_file + self._config_files:
493 495 try:
494 496 yield ConfigFile.from_path(path)
495 497 except (IOError, OSError, ValueError):
496 498 continue
497 499
498 500 for config in gen_configs():
499 501 try:
500 502 return config.get(section, name)
501 503 except KeyError:
502 504 continue
503 505 return None
504 506
505 507 def get_user_name(self, config_file=None):
506 508 """
507 509 Returns user's name from global configuration file.
508 510
509 511 :param config_file: A path to file which should be used to retrieve
510 512 configuration from (might also be a list of file paths)
511 513 """
512 514 return self.get_config_value('user', 'name', config_file)
513 515
514 516 def get_user_email(self, config_file=None):
515 517 """
516 518 Returns user's email from global configuration file.
517 519
518 520 :param config_file: A path to file which should be used to retrieve
519 521 configuration from (might also be a list of file paths)
520 522 """
521 523 return self.get_config_value('user', 'email', config_file)
@@ -1,110 +1,112 b''
1 1 import datetime
2 2 import errno
3 3
4 4 from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset
5 5 from rhodecode.lib.vcs.exceptions import RepositoryError
6 6
7 from ...utils.hgcompat import memfilectx, memctx, hex
7 from ...utils.hgcompat import memfilectx, memctx, hex, tolocal
8 8
9 9
10 10 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
11 11
12 12 def commit(self, message, author, parents=None, branch=None, date=None,
13 13 **kwargs):
14 14 """
15 15 Performs in-memory commit (doesn't check workdir in any way) and
16 16 returns newly created ``Changeset``. Updates repository's
17 17 ``revisions``.
18 18
19 19 :param message: message of the commit
20 20 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
21 21 :param parents: single parent or sequence of parents from which commit
22 22 would be derieved
23 23 :param date: ``datetime.datetime`` instance. Defaults to
24 24 ``datetime.datetime.now()``.
25 25 :param branch: branch name, as string. If none given, default backend's
26 26 branch would be used.
27 27
28 28 :raises ``CommitError``: if any error occurs while committing
29 29 """
30 30 self.check_integrity(parents)
31 31
32 32 from .repository import MercurialRepository
33 if not isinstance(message, str) or not isinstance(author, str):
33 if not isinstance(message, unicode) or not isinstance(author, unicode):
34 34 raise RepositoryError('Given message and author needs to be '
35 'an <str> instance')
35 'an <unicode> instance')
36 36
37 37 if branch is None:
38 38 branch = MercurialRepository.DEFAULT_BRANCH_NAME
39 39 kwargs['branch'] = branch
40 40
41 41 def filectxfn(_repo, memctx, path):
42 42 """
43 43 Marks given path as added/changed/removed in a given _repo. This is
44 44 for internal mercurial commit function.
45 45 """
46 46
47 47 # check if this path is removed
48 48 if path in (node.path for node in self.removed):
49 49 # Raising exception is a way to mark node for removal
50 50 raise IOError(errno.ENOENT, '%s is deleted' % path)
51 51
52 52 # check if this path is added
53 53 for node in self.added:
54 54 if node.path == path:
55 55 return memfilectx(path=node.path,
56 56 data=(node.content.encode('utf8')
57 57 if not node.is_binary else node.content),
58 58 islink=False,
59 59 isexec=node.is_executable,
60 60 copied=False)
61 61
62 62 # or changed
63 63 for node in self.changed:
64 64 if node.path == path:
65 65 return memfilectx(path=node.path,
66 66 data=(node.content.encode('utf8')
67 67 if not node.is_binary else node.content),
68 68 islink=False,
69 69 isexec=node.is_executable,
70 70 copied=False)
71 71
72 72 raise RepositoryError("Given path haven't been marked as added,"
73 73 "changed or removed (%s)" % path)
74 74
75 75 parents = [None, None]
76 76 for i, parent in enumerate(self.parents):
77 77 if parent is not None:
78 78 parents[i] = parent._ctx.node()
79 79
80 80 if date and isinstance(date, datetime.datetime):
81 81 date = date.ctime()
82 82
83 83 commit_ctx = memctx(repo=self.repository._repo,
84 84 parents=parents,
85 85 text='',
86 86 files=self.get_paths(),
87 87 filectxfn=filectxfn,
88 88 user=author,
89 89 date=date,
90 90 extra=kwargs)
91 91
92 loc = lambda u: tolocal(u.encode('utf-8'))
93
92 94 # injecting given _repo params
93 commit_ctx._text = message
94 commit_ctx._user = author
95 commit_ctx._text = loc(message)
96 commit_ctx._user = loc(author)
95 97 commit_ctx._date = date
96 98
97 99 # TODO: Catch exceptions!
98 100 n = self.repository._repo.commitctx(commit_ctx)
99 101 # Returns mercurial node
100 102 self._commit_ctx = commit_ctx # For reference
101 103 # Update vcs repository object & recreate mercurial _repo
102 104 # new_ctx = self.repository._repo[node]
103 105 # new_tip = self.repository.get_changeset(new_ctx.hex())
104 106 new_id = hex(n)
105 107 self.repository.revisions.append(new_id)
106 108 self._repo = self.repository._get_repo(create=False)
107 109 self.repository.branches = self.repository._get_branches()
108 110 tip = self.repository.get_changeset()
109 111 self.reset()
110 112 return tip
@@ -1,559 +1,563 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 stat
12 12 import posixpath
13 13 import mimetypes
14 14
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils import safe_unicode
17 17 from rhodecode.lib.vcs.exceptions import NodeError
18 18 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
19 19
20 20 from pygments import lexers
21 21
22 22
23 23 class NodeKind:
24 24 DIR = 1
25 25 FILE = 2
26 26
27 27
28 28 class NodeState:
29 29 ADDED = u'added'
30 30 CHANGED = u'changed'
31 31 NOT_CHANGED = u'not changed'
32 32 REMOVED = u'removed'
33 33
34 34
35 35 class NodeGeneratorBase(object):
36 36 """
37 37 Base class for removed added and changed filenodes, it's a lazy generator
38 38 class that will create filenodes only on iteration or call
39 39
40 40 The len method doesn't need to create filenodes at all
41 41 """
42 42
43 43 def __init__(self, current_paths, cs):
44 44 self.cs = cs
45 45 self.current_paths = current_paths
46 46
47 47 def __call__(self):
48 48 return [n for n in self]
49 49
50 50 def __getslice__(self, i, j):
51 51 for p in self.current_paths[i:j]:
52 52 yield self.cs.get_node(p)
53 53
54 54 def __len__(self):
55 55 return len(self.current_paths)
56 56
57 57 def __iter__(self):
58 58 for p in self.current_paths:
59 59 yield self.cs.get_node(p)
60 60
61 61
62 62 class AddedFileNodesGenerator(NodeGeneratorBase):
63 63 """
64 64 Class holding Added files for current changeset
65 65 """
66 66 pass
67 67
68 68
69 69 class ChangedFileNodesGenerator(NodeGeneratorBase):
70 70 """
71 71 Class holding Changed files for current changeset
72 72 """
73 73 pass
74 74
75 75
76 76 class RemovedFileNodesGenerator(NodeGeneratorBase):
77 77 """
78 78 Class holding removed files for current changeset
79 79 """
80 80 def __iter__(self):
81 81 for p in self.current_paths:
82 82 yield RemovedFileNode(path=p)
83 83
84 84 def __getslice__(self, i, j):
85 85 for p in self.current_paths[i:j]:
86 86 yield RemovedFileNode(path=p)
87 87
88 88
89 89 class Node(object):
90 90 """
91 91 Simplest class representing file or directory on repository. SCM backends
92 92 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
93 93 directly.
94 94
95 95 Node's ``path`` cannot start with slash as we operate on *relative* paths
96 96 only. Moreover, every single node is identified by the ``path`` attribute,
97 97 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
98 98 """
99 99
100 100 def __init__(self, path, kind):
101 101 if path.startswith('/'):
102 102 raise NodeError("Cannot initialize Node objects with slash at "
103 103 "the beginning as only relative paths are supported")
104 104 self.path = path.rstrip('/')
105 105 if path == '' and kind != NodeKind.DIR:
106 106 raise NodeError("Only DirNode and its subclasses may be "
107 107 "initialized with empty path")
108 108 self.kind = kind
109 109 #self.dirs, self.files = [], []
110 110 if self.is_root() and not self.is_dir():
111 111 raise NodeError("Root node cannot be FILE kind")
112 112
113 113 @LazyProperty
114 114 def parent(self):
115 115 parent_path = self.get_parent_path()
116 116 if parent_path:
117 117 if self.changeset:
118 118 return self.changeset.get_node(parent_path)
119 119 return DirNode(parent_path)
120 120 return None
121 121
122 122 @LazyProperty
123 def unicode_path(self):
124 return safe_unicode(self.path)
125
126 @LazyProperty
123 127 def name(self):
124 128 """
125 129 Returns name of the node so if its path
126 130 then only last part is returned.
127 131 """
128 132 return safe_unicode(self.path.rstrip('/').split('/')[-1])
129 133
130 134 def _get_kind(self):
131 135 return self._kind
132 136
133 137 def _set_kind(self, kind):
134 138 if hasattr(self, '_kind'):
135 139 raise NodeError("Cannot change node's kind")
136 140 else:
137 141 self._kind = kind
138 142 # Post setter check (path's trailing slash)
139 143 if self.path.endswith('/'):
140 144 raise NodeError("Node's path cannot end with slash")
141 145
142 146 kind = property(_get_kind, _set_kind)
143 147
144 148 def __cmp__(self, other):
145 149 """
146 150 Comparator using name of the node, needed for quick list sorting.
147 151 """
148 152 kind_cmp = cmp(self.kind, other.kind)
149 153 if kind_cmp:
150 154 return kind_cmp
151 155 return cmp(self.name, other.name)
152 156
153 157 def __eq__(self, other):
154 158 for attr in ['name', 'path', 'kind']:
155 159 if getattr(self, attr) != getattr(other, attr):
156 160 return False
157 161 if self.is_file():
158 162 if self.content != other.content:
159 163 return False
160 164 else:
161 165 # For DirNode's check without entering each dir
162 166 self_nodes_paths = list(sorted(n.path for n in self.nodes))
163 167 other_nodes_paths = list(sorted(n.path for n in self.nodes))
164 168 if self_nodes_paths != other_nodes_paths:
165 169 return False
166 170 return True
167 171
168 172 def __nq__(self, other):
169 173 return not self.__eq__(other)
170 174
171 175 def __repr__(self):
172 176 return '<%s %r>' % (self.__class__.__name__, self.path)
173 177
174 178 def __str__(self):
175 179 return self.__repr__()
176 180
177 181 def __unicode__(self):
178 182 return self.name
179 183
180 184 def get_parent_path(self):
181 185 """
182 186 Returns node's parent path or empty string if node is root.
183 187 """
184 188 if self.is_root():
185 189 return ''
186 190 return posixpath.dirname(self.path.rstrip('/')) + '/'
187 191
188 192 def is_file(self):
189 193 """
190 194 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
191 195 otherwise.
192 196 """
193 197 return self.kind == NodeKind.FILE
194 198
195 199 def is_dir(self):
196 200 """
197 201 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
198 202 otherwise.
199 203 """
200 204 return self.kind == NodeKind.DIR
201 205
202 206 def is_root(self):
203 207 """
204 208 Returns ``True`` if node is a root node and ``False`` otherwise.
205 209 """
206 210 return self.kind == NodeKind.DIR and self.path == ''
207 211
208 212 @LazyProperty
209 213 def added(self):
210 214 return self.state is NodeState.ADDED
211 215
212 216 @LazyProperty
213 217 def changed(self):
214 218 return self.state is NodeState.CHANGED
215 219
216 220 @LazyProperty
217 221 def not_changed(self):
218 222 return self.state is NodeState.NOT_CHANGED
219 223
220 224 @LazyProperty
221 225 def removed(self):
222 226 return self.state is NodeState.REMOVED
223 227
224 228
225 229 class FileNode(Node):
226 230 """
227 231 Class representing file nodes.
228 232
229 233 :attribute: path: path to the node, relative to repostiory's root
230 234 :attribute: content: if given arbitrary sets content of the file
231 235 :attribute: changeset: if given, first time content is accessed, callback
232 236 :attribute: mode: octal stat mode for a node. Default is 0100644.
233 237 """
234 238
235 239 def __init__(self, path, content=None, changeset=None, mode=None):
236 240 """
237 241 Only one of ``content`` and ``changeset`` may be given. Passing both
238 242 would raise ``NodeError`` exception.
239 243
240 244 :param path: relative path to the node
241 245 :param content: content may be passed to constructor
242 246 :param changeset: if given, will use it to lazily fetch content
243 247 :param mode: octal representation of ST_MODE (i.e. 0100644)
244 248 """
245 249
246 250 if content and changeset:
247 251 raise NodeError("Cannot use both content and changeset")
248 252 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
249 253 self.changeset = changeset
250 254 self._content = content
251 255 self._mode = mode or 0100644
252 256
253 257 @LazyProperty
254 258 def mode(self):
255 259 """
256 260 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
257 261 use value given at initialization or 0100644 (default).
258 262 """
259 263 if self.changeset:
260 264 mode = self.changeset.get_file_mode(self.path)
261 265 else:
262 266 mode = self._mode
263 267 return mode
264 268
265 269 @property
266 270 def content(self):
267 271 """
268 272 Returns lazily content of the FileNode. If possible, would try to
269 273 decode content from UTF-8.
270 274 """
271 275 if self.changeset:
272 276 content = self.changeset.get_file_content(self.path)
273 277 else:
274 278 content = self._content
275 279
276 280 if bool(content and '\0' in content):
277 281 return content
278 282 return safe_unicode(content)
279 283
280 284 @LazyProperty
281 285 def size(self):
282 286 if self.changeset:
283 287 return self.changeset.get_file_size(self.path)
284 288 raise NodeError("Cannot retrieve size of the file without related "
285 289 "changeset attribute")
286 290
287 291 @LazyProperty
288 292 def message(self):
289 293 if self.changeset:
290 294 return self.last_changeset.message
291 295 raise NodeError("Cannot retrieve message of the file without related "
292 296 "changeset attribute")
293 297
294 298 @LazyProperty
295 299 def last_changeset(self):
296 300 if self.changeset:
297 301 return self.changeset.get_file_changeset(self.path)
298 302 raise NodeError("Cannot retrieve last changeset of the file without "
299 303 "related changeset attribute")
300 304
301 305 def get_mimetype(self):
302 306 """
303 307 Mimetype is calculated based on the file's content. If ``_mimetype``
304 308 attribute is available, it will be returned (backends which store
305 309 mimetypes or can easily recognize them, should set this private
306 310 attribute to indicate that type should *NOT* be calculated).
307 311 """
308 312 if hasattr(self, '_mimetype'):
309 313 if (isinstance(self._mimetype, (tuple, list,)) and
310 314 len(self._mimetype) == 2):
311 315 return self._mimetype
312 316 else:
313 317 raise NodeError('given _mimetype attribute must be an 2 '
314 318 'element list or tuple')
315 319
316 320 mtype, encoding = mimetypes.guess_type(self.name)
317 321
318 322 if mtype is None:
319 323 if self.is_binary:
320 324 mtype = 'application/octet-stream'
321 325 encoding = None
322 326 else:
323 327 mtype = 'text/plain'
324 328 encoding = None
325 329 return mtype, encoding
326 330
327 331 @LazyProperty
328 332 def mimetype(self):
329 333 """
330 334 Wrapper around full mimetype info. It returns only type of fetched
331 335 mimetype without the encoding part. use get_mimetype function to fetch
332 336 full set of (type,encoding)
333 337 """
334 338 return self.get_mimetype()[0]
335 339
336 340 @LazyProperty
337 341 def mimetype_main(self):
338 342 return self.mimetype.split('/')[0]
339 343
340 344 @LazyProperty
341 345 def lexer(self):
342 346 """
343 347 Returns pygment's lexer class. Would try to guess lexer taking file's
344 348 content, name and mimetype.
345 349 """
346 350 try:
347 351 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
348 352 except lexers.ClassNotFound:
349 353 lexer = lexers.TextLexer()
350 354 # returns first alias
351 355 return lexer
352 356
353 357 @LazyProperty
354 358 def lexer_alias(self):
355 359 """
356 360 Returns first alias of the lexer guessed for this file.
357 361 """
358 362 return self.lexer.aliases[0]
359 363
360 364 @LazyProperty
361 365 def history(self):
362 366 """
363 367 Returns a list of changeset for this file in which the file was changed
364 368 """
365 369 if self.changeset is None:
366 370 raise NodeError('Unable to get changeset for this FileNode')
367 371 return self.changeset.get_file_history(self.path)
368 372
369 373 @LazyProperty
370 374 def annotate(self):
371 375 """
372 376 Returns a list of three element tuples with lineno,changeset and line
373 377 """
374 378 if self.changeset is None:
375 379 raise NodeError('Unable to get changeset for this FileNode')
376 380 return self.changeset.get_file_annotate(self.path)
377 381
378 382 @LazyProperty
379 383 def state(self):
380 384 if not self.changeset:
381 385 raise NodeError("Cannot check state of the node if it's not "
382 386 "linked with changeset")
383 387 elif self.path in (node.path for node in self.changeset.added):
384 388 return NodeState.ADDED
385 389 elif self.path in (node.path for node in self.changeset.changed):
386 390 return NodeState.CHANGED
387 391 else:
388 392 return NodeState.NOT_CHANGED
389 393
390 394 @property
391 395 def is_binary(self):
392 396 """
393 397 Returns True if file has binary content.
394 398 """
395 399 _bin = '\0' in self.content
396 400 return _bin
397 401
398 402 @LazyProperty
399 403 def extension(self):
400 404 """Returns filenode extension"""
401 405 return self.name.split('.')[-1]
402 406
403 407 def is_executable(self):
404 408 """
405 409 Returns ``True`` if file has executable flag turned on.
406 410 """
407 411 return bool(self.mode & stat.S_IXUSR)
408 412
409 413 def __repr__(self):
410 414 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
411 415 self.changeset.short_id)
412 416
413 417
414 418 class RemovedFileNode(FileNode):
415 419 """
416 420 Dummy FileNode class - trying to access any public attribute except path,
417 421 name, kind or state (or methods/attributes checking those two) would raise
418 422 RemovedFileNodeError.
419 423 """
420 424 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
421 425 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
422 426
423 427 def __init__(self, path):
424 428 """
425 429 :param path: relative path to the node
426 430 """
427 431 super(RemovedFileNode, self).__init__(path=path)
428 432
429 433 def __getattribute__(self, attr):
430 434 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
431 435 return super(RemovedFileNode, self).__getattribute__(attr)
432 436 raise RemovedFileNodeError("Cannot access attribute %s on "
433 437 "RemovedFileNode" % attr)
434 438
435 439 @LazyProperty
436 440 def state(self):
437 441 return NodeState.REMOVED
438 442
439 443
440 444 class DirNode(Node):
441 445 """
442 446 DirNode stores list of files and directories within this node.
443 447 Nodes may be used standalone but within repository context they
444 448 lazily fetch data within same repositorty's changeset.
445 449 """
446 450
447 451 def __init__(self, path, nodes=(), changeset=None):
448 452 """
449 453 Only one of ``nodes`` and ``changeset`` may be given. Passing both
450 454 would raise ``NodeError`` exception.
451 455
452 456 :param path: relative path to the node
453 457 :param nodes: content may be passed to constructor
454 458 :param changeset: if given, will use it to lazily fetch content
455 459 :param size: always 0 for ``DirNode``
456 460 """
457 461 if nodes and changeset:
458 462 raise NodeError("Cannot use both nodes and changeset")
459 463 super(DirNode, self).__init__(path, NodeKind.DIR)
460 464 self.changeset = changeset
461 465 self._nodes = nodes
462 466
463 467 @LazyProperty
464 468 def content(self):
465 469 raise NodeError("%s represents a dir and has no ``content`` attribute"
466 470 % self)
467 471
468 472 @LazyProperty
469 473 def nodes(self):
470 474 if self.changeset:
471 475 nodes = self.changeset.get_nodes(self.path)
472 476 else:
473 477 nodes = self._nodes
474 478 self._nodes_dict = dict((node.path, node) for node in nodes)
475 479 return sorted(nodes)
476 480
477 481 @LazyProperty
478 482 def files(self):
479 483 return sorted((node for node in self.nodes if node.is_file()))
480 484
481 485 @LazyProperty
482 486 def dirs(self):
483 487 return sorted((node for node in self.nodes if node.is_dir()))
484 488
485 489 def __iter__(self):
486 490 for node in self.nodes:
487 491 yield node
488 492
489 493 def get_node(self, path):
490 494 """
491 495 Returns node from within this particular ``DirNode``, so it is now
492 496 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
493 497 'docs'. In order to access deeper nodes one must fetch nodes between
494 498 them first - this would work::
495 499
496 500 docs = root.get_node('docs')
497 501 docs.get_node('api').get_node('index.rst')
498 502
499 503 :param: path - relative to the current node
500 504
501 505 .. note::
502 506 To access lazily (as in example above) node have to be initialized
503 507 with related changeset object - without it node is out of
504 508 context and may know nothing about anything else than nearest
505 509 (located at same level) nodes.
506 510 """
507 511 try:
508 512 path = path.rstrip('/')
509 513 if path == '':
510 514 raise NodeError("Cannot retrieve node without path")
511 515 self.nodes # access nodes first in order to set _nodes_dict
512 516 paths = path.split('/')
513 517 if len(paths) == 1:
514 518 if not self.is_root():
515 519 path = '/'.join((self.path, paths[0]))
516 520 else:
517 521 path = paths[0]
518 522 return self._nodes_dict[path]
519 523 elif len(paths) > 1:
520 524 if self.changeset is None:
521 525 raise NodeError("Cannot access deeper "
522 526 "nodes without changeset")
523 527 else:
524 528 path1, path2 = paths[0], '/'.join(paths[1:])
525 529 return self.get_node(path1).get_node(path2)
526 530 else:
527 531 raise KeyError
528 532 except KeyError:
529 533 raise NodeError("Node does not exist at %s" % path)
530 534
531 535 @LazyProperty
532 536 def state(self):
533 537 raise NodeError("Cannot access state of DirNode")
534 538
535 539 @LazyProperty
536 540 def size(self):
537 541 size = 0
538 542 for root, dirs, files in self.changeset.walk(self.path):
539 543 for f in files:
540 544 size += f.size
541 545
542 546 return size
543 547
544 548 def __repr__(self):
545 549 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
546 550 self.changeset.short_id)
547 551
548 552
549 553 class RootNode(DirNode):
550 554 """
551 555 DirNode being the root node of the repository.
552 556 """
553 557
554 558 def __init__(self, nodes=(), changeset=None):
555 559 super(RootNode, self).__init__(path='', nodes=nodes,
556 560 changeset=changeset)
557 561
558 562 def __repr__(self):
559 563 return '<%s>' % self.__class__.__name__
@@ -1,12 +1,14 b''
1 """Mercurial libs compatibility
1 """
2 Mercurial libs compatibility
3 """
2 4
3 """
4 5 from mercurial import archival, merge as hg_merge, patch, ui
5 6 from mercurial.commands import clone, nullid, pull
6 7 from mercurial.context import memctx, memfilectx
7 8 from mercurial.error import RepoError, RepoLookupError, Abort
8 9 from mercurial.hgweb.common import get_contact
9 10 from mercurial.localrepo import localrepository
10 11 from mercurial.match import match
11 12 from mercurial.mdiff import diffopts
12 13 from mercurial.node import hex
14 from mercurial.encoding import tolocal No newline at end of file
@@ -1,459 +1,461 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 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29 import cStringIO
30 30
31 31 from rhodecode.lib.vcs import get_backend
32 32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 34 from rhodecode.lib.vcs.nodes import FileNode
35 35
36 36 from rhodecode import BACKENDS
37 37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.utils2 import safe_str
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 39 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
40 40 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
41 41 action_logger, EmptyChangeset, REMOVED_REPO_PAT
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
44 44 UserFollowing, UserLog, User, RepoGroup
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class UserTemp(object):
50 50 def __init__(self, user_id):
51 51 self.user_id = user_id
52 52
53 53 def __repr__(self):
54 54 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
55 55
56 56
57 57 class RepoTemp(object):
58 58 def __init__(self, repo_id):
59 59 self.repo_id = repo_id
60 60
61 61 def __repr__(self):
62 62 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
63 63
64 64
65 65 class CachedRepoList(object):
66 66
67 67 def __init__(self, db_repo_list, repos_path, order_by=None):
68 68 self.db_repo_list = db_repo_list
69 69 self.repos_path = repos_path
70 70 self.order_by = order_by
71 71 self.reversed = (order_by or '').startswith('-')
72 72
73 73 def __len__(self):
74 74 return len(self.db_repo_list)
75 75
76 76 def __repr__(self):
77 77 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
78 78
79 79 def __iter__(self):
80 80 for dbr in self.db_repo_list:
81 81 scmr = dbr.scm_instance_cached
82 82 # check permission at this level
83 83 if not HasRepoPermissionAny(
84 84 'repository.read', 'repository.write', 'repository.admin'
85 85 )(dbr.repo_name, 'get repo check'):
86 86 continue
87 87
88 88 if scmr is None:
89 89 log.error(
90 90 '%s this repository is present in database but it '
91 91 'cannot be created as an scm instance' % dbr.repo_name
92 92 )
93 93 continue
94 94
95 95 last_change = scmr.last_change
96 96 tip = h.get_changeset_safe(scmr, 'tip')
97 97
98 98 tmp_d = {}
99 99 tmp_d['name'] = dbr.repo_name
100 100 tmp_d['name_sort'] = tmp_d['name'].lower()
101 101 tmp_d['description'] = dbr.description
102 102 tmp_d['description_sort'] = tmp_d['description']
103 103 tmp_d['last_change'] = last_change
104 104 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
105 105 tmp_d['tip'] = tip.raw_id
106 106 tmp_d['tip_sort'] = tip.revision
107 107 tmp_d['rev'] = tip.revision
108 108 tmp_d['contact'] = dbr.user.full_contact
109 109 tmp_d['contact_sort'] = tmp_d['contact']
110 110 tmp_d['owner_sort'] = tmp_d['contact']
111 111 tmp_d['repo_archives'] = list(scmr._get_archives())
112 112 tmp_d['last_msg'] = tip.message
113 113 tmp_d['author'] = tip.author
114 114 tmp_d['dbrepo'] = dbr.get_dict()
115 115 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
116 116 yield tmp_d
117 117
118 118
119 119 class GroupList(object):
120 120
121 121 def __init__(self, db_repo_group_list):
122 122 self.db_repo_group_list = db_repo_group_list
123 123
124 124 def __len__(self):
125 125 return len(self.db_repo_group_list)
126 126
127 127 def __repr__(self):
128 128 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
129 129
130 130 def __iter__(self):
131 131 for dbgr in self.db_repo_group_list:
132 132 # check permission at this level
133 133 if not HasReposGroupPermissionAny(
134 134 'group.read', 'group.write', 'group.admin'
135 135 )(dbgr.group_name, 'get group repo check'):
136 136 continue
137 137
138 138 yield dbgr
139 139
140 140
141 141 class ScmModel(BaseModel):
142 142 """
143 143 Generic Scm Model
144 144 """
145 145
146 146 def __get_repo(self, instance):
147 147 cls = Repository
148 148 if isinstance(instance, cls):
149 149 return instance
150 150 elif isinstance(instance, int) or str(instance).isdigit():
151 151 return cls.get(instance)
152 152 elif isinstance(instance, basestring):
153 153 return cls.get_by_repo_name(instance)
154 154 elif instance:
155 155 raise Exception('given object must be int, basestr or Instance'
156 156 ' of %s got %s' % (type(cls), type(instance)))
157 157
158 158 @LazyProperty
159 159 def repos_path(self):
160 160 """
161 161 Get's the repositories root path from database
162 162 """
163 163
164 164 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
165 165
166 166 return q.ui_value
167 167
168 168 def repo_scan(self, repos_path=None):
169 169 """
170 170 Listing of repositories in given path. This path should not be a
171 171 repository itself. Return a dictionary of repository objects
172 172
173 173 :param repos_path: path to directory containing repositories
174 174 """
175 175
176 176 if repos_path is None:
177 177 repos_path = self.repos_path
178 178
179 179 log.info('scanning for repositories in %s' % repos_path)
180 180
181 181 baseui = make_ui('db')
182 182 repos = {}
183 183
184 184 for name, path in get_filesystem_repos(repos_path, recursive=True):
185 185 # skip removed repos
186 186 if REMOVED_REPO_PAT.match(name):
187 187 continue
188 188
189 189 # name need to be decomposed and put back together using the /
190 190 # since this is internal storage separator for rhodecode
191 191 name = Repository.url_sep().join(name.split(os.sep))
192 192
193 193 try:
194 194 if name in repos:
195 195 raise RepositoryError('Duplicate repository name %s '
196 196 'found in %s' % (name, path))
197 197 else:
198 198
199 199 klass = get_backend(path[0])
200 200
201 201 if path[0] == 'hg' and path[0] in BACKENDS.keys():
202 202 repos[name] = klass(safe_str(path[1]), baseui=baseui)
203 203
204 204 if path[0] == 'git' and path[0] in BACKENDS.keys():
205 205 repos[name] = klass(path[1])
206 206 except OSError:
207 207 continue
208 208
209 209 return repos
210 210
211 211 def get_repos(self, all_repos=None, sort_key=None):
212 212 """
213 213 Get all repos from db and for each repo create it's
214 214 backend instance and fill that backed with information from database
215 215
216 216 :param all_repos: list of repository names as strings
217 217 give specific repositories list, good for filtering
218 218 """
219 219 if all_repos is None:
220 220 all_repos = self.sa.query(Repository)\
221 221 .filter(Repository.group_id == None)\
222 222 .order_by(Repository.repo_name).all()
223 223
224 224 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
225 225 order_by=sort_key)
226 226
227 227 return repo_iter
228 228
229 229 def get_repos_groups(self, all_groups=None):
230 230 if all_groups is None:
231 231 all_groups = RepoGroup.query()\
232 232 .filter(RepoGroup.group_parent_id == None).all()
233 233 group_iter = GroupList(all_groups)
234 234
235 235 return group_iter
236 236
237 237 def mark_for_invalidation(self, repo_name):
238 238 """
239 239 Puts cache invalidation task into db for
240 240 further global cache invalidation
241 241
242 242 :param repo_name: this repo that should invalidation take place
243 243 """
244 244 CacheInvalidation.set_invalidate(repo_name)
245 245
246 246 def toggle_following_repo(self, follow_repo_id, user_id):
247 247
248 248 f = self.sa.query(UserFollowing)\
249 249 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
250 250 .filter(UserFollowing.user_id == user_id).scalar()
251 251
252 252 if f is not None:
253 253 try:
254 254 self.sa.delete(f)
255 255 action_logger(UserTemp(user_id),
256 256 'stopped_following_repo',
257 257 RepoTemp(follow_repo_id))
258 258 return
259 259 except:
260 260 log.error(traceback.format_exc())
261 261 raise
262 262
263 263 try:
264 264 f = UserFollowing()
265 265 f.user_id = user_id
266 266 f.follows_repo_id = follow_repo_id
267 267 self.sa.add(f)
268 268
269 269 action_logger(UserTemp(user_id),
270 270 'started_following_repo',
271 271 RepoTemp(follow_repo_id))
272 272 except:
273 273 log.error(traceback.format_exc())
274 274 raise
275 275
276 276 def toggle_following_user(self, follow_user_id, user_id):
277 277 f = self.sa.query(UserFollowing)\
278 278 .filter(UserFollowing.follows_user_id == follow_user_id)\
279 279 .filter(UserFollowing.user_id == user_id).scalar()
280 280
281 281 if f is not None:
282 282 try:
283 283 self.sa.delete(f)
284 284 return
285 285 except:
286 286 log.error(traceback.format_exc())
287 287 raise
288 288
289 289 try:
290 290 f = UserFollowing()
291 291 f.user_id = user_id
292 292 f.follows_user_id = follow_user_id
293 293 self.sa.add(f)
294 294 except:
295 295 log.error(traceback.format_exc())
296 296 raise
297 297
298 298 def is_following_repo(self, repo_name, user_id, cache=False):
299 299 r = self.sa.query(Repository)\
300 300 .filter(Repository.repo_name == repo_name).scalar()
301 301
302 302 f = self.sa.query(UserFollowing)\
303 303 .filter(UserFollowing.follows_repository == r)\
304 304 .filter(UserFollowing.user_id == user_id).scalar()
305 305
306 306 return f is not None
307 307
308 308 def is_following_user(self, username, user_id, cache=False):
309 309 u = User.get_by_username(username)
310 310
311 311 f = self.sa.query(UserFollowing)\
312 312 .filter(UserFollowing.follows_user == u)\
313 313 .filter(UserFollowing.user_id == user_id).scalar()
314 314
315 315 return f is not None
316 316
317 317 def get_followers(self, repo_id):
318 318 if not isinstance(repo_id, int):
319 319 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
320 320
321 321 return self.sa.query(UserFollowing)\
322 322 .filter(UserFollowing.follows_repo_id == repo_id).count()
323 323
324 324 def get_forks(self, repo_id):
325 325 if not isinstance(repo_id, int):
326 326 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
327 327
328 328 return self.sa.query(Repository)\
329 329 .filter(Repository.fork_id == repo_id).count()
330 330
331 331 def mark_as_fork(self, repo, fork, user):
332 332 repo = self.__get_repo(repo)
333 333 fork = self.__get_repo(fork)
334 334 repo.fork = fork
335 335 self.sa.add(repo)
336 336 return repo
337 337
338 338 def pull_changes(self, repo_name, username):
339 339 dbrepo = Repository.get_by_repo_name(repo_name)
340 340 clone_uri = dbrepo.clone_uri
341 341 if not clone_uri:
342 342 raise Exception("This repository doesn't have a clone uri")
343 343
344 344 repo = dbrepo.scm_instance
345 345 try:
346 346 extras = {'ip': '',
347 347 'username': username,
348 348 'action': 'push_remote',
349 349 'repository': repo_name}
350 350
351 351 #inject ui extra param to log this action via push logger
352 352 for k, v in extras.items():
353 353 repo._repo.ui.setconfig('rhodecode_extras', k, v)
354 354
355 355 repo.pull(clone_uri)
356 356 self.mark_for_invalidation(repo_name)
357 357 except:
358 358 log.error(traceback.format_exc())
359 359 raise
360 360
361 361 def commit_change(self, repo, repo_name, cs, user, author, message,
362 362 content, f_path):
363 363
364 364 if repo.alias == 'hg':
365 365 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
366 366 elif repo.alias == 'git':
367 367 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
368 368
369 369 # decoding here will force that we have proper encoded values
370 370 # in any other case this will throw exceptions and deny commit
371 371 content = safe_str(content)
372 message = safe_str(message)
373 372 path = safe_str(f_path)
374 author = safe_str(author)
373 # message and author needs to be unicode
374 # proper backend should then translate that into required type
375 message = safe_unicode(message)
376 author = safe_unicode(author)
375 377 m = IMC(repo)
376 378 m.change(FileNode(path, content))
377 379 tip = m.commit(message=message,
378 380 author=author,
379 381 parents=[cs], branch=cs.branch)
380 382
381 383 new_cs = tip.short_id
382 384 action = 'push_local:%s' % new_cs
383 385
384 386 action_logger(user, action, repo_name)
385 387
386 388 self.mark_for_invalidation(repo_name)
387 389
388 390 def create_node(self, repo, repo_name, cs, user, author, message, content,
389 391 f_path):
390 392 if repo.alias == 'hg':
391 393 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
392 394 elif repo.alias == 'git':
393 395 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
394 396 # decoding here will force that we have proper encoded values
395 397 # in any other case this will throw exceptions and deny commit
396 398
397 399 if isinstance(content, (basestring,)):
398 400 content = safe_str(content)
399 401 elif isinstance(content, (file, cStringIO.OutputType,)):
400 402 content = content.read()
401 403 else:
402 404 raise Exception('Content is of unrecognized type %s' % (
403 405 type(content)
404 406 ))
405 407
406 message = safe_str(message)
408 message = safe_unicode(message)
409 author = safe_unicode(author)
407 410 path = safe_str(f_path)
408 author = safe_str(author)
409 411 m = IMC(repo)
410 412
411 413 if isinstance(cs, EmptyChangeset):
412 # Emptychangeset means we we're editing empty repository
414 # EmptyChangeset means we we're editing empty repository
413 415 parents = None
414 416 else:
415 417 parents = [cs]
416 418
417 419 m.add(FileNode(path, content=content))
418 420 tip = m.commit(message=message,
419 421 author=author,
420 422 parents=parents, branch=cs.branch)
421 423 new_cs = tip.short_id
422 424 action = 'push_local:%s' % new_cs
423 425
424 426 action_logger(user, action, repo_name)
425 427
426 428 self.mark_for_invalidation(repo_name)
427 429
428 430 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
429 431 """
430 432 recursive walk in root dir and return a set of all path in that dir
431 433 based on repository walk function
432 434
433 435 :param repo_name: name of repository
434 436 :param revision: revision for which to list nodes
435 437 :param root_path: root path to list
436 438 :param flat: return as a list, if False returns a dict with decription
437 439
438 440 """
439 441 _files = list()
440 442 _dirs = list()
441 443 try:
442 444 _repo = self.__get_repo(repo_name)
443 445 changeset = _repo.scm_instance.get_changeset(revision)
444 446 root_path = root_path.lstrip('/')
445 447 for topnode, dirs, files in changeset.walk(root_path):
446 448 for f in files:
447 449 _files.append(f.path if flat else {"name": f.path,
448 450 "type": "file"})
449 451 for d in dirs:
450 452 _dirs.append(d.path if flat else {"name": d.path,
451 453 "type": "dir"})
452 454 except RepositoryError:
453 455 log.debug(traceback.format_exc())
454 456 raise
455 457
456 458 return _dirs, _files
457 459
458 460 def get_unread_journal(self):
459 461 return self.sa.query(UserLog).count()
@@ -1,78 +1,78 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Edit file')} - ${c.rhodecode_name}
5 5 </%def>
6 6
7 7 <%def name="js_extra()">
8 8 <script type="text/javascript" src="${h.url('/js/codemirror.js')}"></script>
9 9 </%def>
10 10 <%def name="css_extra()">
11 11 <link rel="stylesheet" type="text/css" href="${h.url('/css/codemirror.css')}"/>
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${h.link_to(u'Home',h.url('/'))}
16 16 &raquo;
17 17 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
18 18 &raquo;
19 19 ${_('edit file')} @ R${c.cs.revision}:${h.short_id(c.cs.raw_id)}
20 20 </%def>
21 21
22 22 <%def name="page_nav()">
23 23 ${self.menu('files')}
24 24 </%def>
25 25 <%def name="main()">
26 26 <div class="box">
27 27 <!-- box / title -->
28 28 <div class="title">
29 29 ${self.breadcrumbs()}
30 30 <ul class="links">
31 31 <li>
32 32 <span style="text-transform: uppercase;">
33 33 <a href="#">${_('branch')}: ${c.cs.branch}</a></span>
34 34 </li>
35 35 </ul>
36 36 </div>
37 37 <div class="table">
38 38 <div id="files_data">
39 39 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.revision,c.file.path)}</h3>
40 40 ${h.form(h.url.current(),method='post',id='eform')}
41 41 <div id="body" class="codeblock">
42 42 <div class="code-header">
43 43 <div class="stats">
44 44 <div class="left"><img src="${h.url('/images/icons/file.png')}"/></div>
45 45 <div class="left item">${h.link_to("r%s:%s" % (c.file.changeset.revision,h.short_id(c.file.changeset.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</div>
46 46 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
47 47 <div class="left item last">${c.file.mimetype}</div>
48 48 <div class="buttons">
49 49 ${h.link_to(_('show annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
50 50 ${h.link_to(_('show as raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
51 51 ${h.link_to(_('download as raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
52 52 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
53 53 % if not c.file.is_binary:
54 54 ${h.link_to(_('source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="ui-btn")}
55 55 % endif
56 56 % endif
57 57 </div>
58 58 </div>
59 <div class="commit">${_('Editing file')}: ${c.file.path}</div>
59 <div class="commit">${_('Editing file')}: ${c.file.unicode_path}</div>
60 60 </div>
61 61 <pre id="editor_pre"></pre>
62 62 <textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
63 63 <div style="padding: 10px;color:#666666">${_('commit message')}</div>
64 64 <textarea id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px"></textarea>
65 65 </div>
66 66 <div style="text-align: left;padding-top: 5px">
67 67 ${h.submit('commit',_('Commit changes'),class_="ui-btn")}
68 68 ${h.reset('reset',_('Reset'),class_="ui-btn")}
69 69 </div>
70 70 ${h.end_form()}
71 71 <script type="text/javascript">
72 72 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
73 73 initCodeMirror('editor',reset_url);
74 74 </script>
75 75 </div>
76 76 </div>
77 77 </div>
78 78 </%def>
General Comments 0
You need to be logged in to leave comments. Login now