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