##// END OF EJS Templates
display current heads of branches for git in changelog and shortlog
marcink -
r2198:9784a54a beta
parent child Browse files
Show More
@@ -1,459 +1,446
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 # TODO: Cache as we walk (id <-> branch name mapping)
69
70 refs = self.repository._repo.get_refs()
70 heads = self.repository._heads(reverse=False)
71 heads = {}
72 for key, val in refs.items():
73 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
74 if key.startswith(ref_key):
75 n = key[len(ref_key):]
76 if n not in ['HEAD']:
77 heads[n] = val
78
71
79 for name, id in heads.iteritems():
72 ref = heads.get(self.raw_id)
80 walker = self.repository._repo.object_store.get_graph_walker([id])
73 if ref:
81 while True:
74 return safe_unicode(ref)
82 id_ = walker.next()
75
83 if not id_:
84 break
85 if id_ == self.id:
86 return safe_unicode(name)
87 raise ChangesetError("This should not happen... Have you manually "
88 "change id of the changeset?")
89
76
90 def _fix_path(self, path):
77 def _fix_path(self, path):
91 """
78 """
92 Paths are stored without trailing slash so we need to get rid off it if
79 Paths are stored without trailing slash so we need to get rid off it if
93 needed.
80 needed.
94 """
81 """
95 if path.endswith('/'):
82 if path.endswith('/'):
96 path = path.rstrip('/')
83 path = path.rstrip('/')
97 return path
84 return path
98
85
99 def _get_id_for_path(self, path):
86 def _get_id_for_path(self, path):
100
87
101 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
88 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
102 if not path in self._paths:
89 if not path in self._paths:
103 path = path.strip('/')
90 path = path.strip('/')
104 # set root tree
91 # set root tree
105 tree = self.repository._repo[self._commit.tree]
92 tree = self.repository._repo[self._commit.tree]
106 if path == '':
93 if path == '':
107 self._paths[''] = tree.id
94 self._paths[''] = tree.id
108 return tree.id
95 return tree.id
109 splitted = path.split('/')
96 splitted = path.split('/')
110 dirs, name = splitted[:-1], splitted[-1]
97 dirs, name = splitted[:-1], splitted[-1]
111 curdir = ''
98 curdir = ''
112
99
113 # initially extract things from root dir
100 # initially extract things from root dir
114 for item, stat, id in tree.iteritems():
101 for item, stat, id in tree.iteritems():
115 if curdir:
102 if curdir:
116 name = '/'.join((curdir, item))
103 name = '/'.join((curdir, item))
117 else:
104 else:
118 name = item
105 name = item
119 self._paths[name] = id
106 self._paths[name] = id
120 self._stat_modes[name] = stat
107 self._stat_modes[name] = stat
121
108
122 for dir in dirs:
109 for dir in dirs:
123 if curdir:
110 if curdir:
124 curdir = '/'.join((curdir, dir))
111 curdir = '/'.join((curdir, dir))
125 else:
112 else:
126 curdir = dir
113 curdir = dir
127 dir_id = None
114 dir_id = None
128 for item, stat, id in tree.iteritems():
115 for item, stat, id in tree.iteritems():
129 if dir == item:
116 if dir == item:
130 dir_id = id
117 dir_id = id
131 if dir_id:
118 if dir_id:
132 # Update tree
119 # Update tree
133 tree = self.repository._repo[dir_id]
120 tree = self.repository._repo[dir_id]
134 if not isinstance(tree, objects.Tree):
121 if not isinstance(tree, objects.Tree):
135 raise ChangesetError('%s is not a directory' % curdir)
122 raise ChangesetError('%s is not a directory' % curdir)
136 else:
123 else:
137 raise ChangesetError('%s have not been found' % curdir)
124 raise ChangesetError('%s have not been found' % curdir)
138
125
139 # cache all items from the given traversed tree
126 # cache all items from the given traversed tree
140 for item, stat, id in tree.iteritems():
127 for item, stat, id in tree.iteritems():
141 if curdir:
128 if curdir:
142 name = '/'.join((curdir, item))
129 name = '/'.join((curdir, item))
143 else:
130 else:
144 name = item
131 name = item
145 self._paths[name] = id
132 self._paths[name] = id
146 self._stat_modes[name] = stat
133 self._stat_modes[name] = stat
147
134
148 if not path in self._paths:
135 if not path in self._paths:
149 raise NodeDoesNotExistError("There is no file nor directory "
136 raise NodeDoesNotExistError("There is no file nor directory "
150 "at the given path %r at revision %r"
137 "at the given path %r at revision %r"
151 % (path, self.short_id))
138 % (path, self.short_id))
152 return self._paths[path]
139 return self._paths[path]
153
140
154 def _get_kind(self, path):
141 def _get_kind(self, path):
155 id = self._get_id_for_path(path)
142 id = self._get_id_for_path(path)
156 obj = self.repository._repo[id]
143 obj = self.repository._repo[id]
157 if isinstance(obj, objects.Blob):
144 if isinstance(obj, objects.Blob):
158 return NodeKind.FILE
145 return NodeKind.FILE
159 elif isinstance(obj, objects.Tree):
146 elif isinstance(obj, objects.Tree):
160 return NodeKind.DIR
147 return NodeKind.DIR
161
148
162 def _get_file_nodes(self):
149 def _get_file_nodes(self):
163 return chain(*(t[2] for t in self.walk()))
150 return chain(*(t[2] for t in self.walk()))
164
151
165 @LazyProperty
152 @LazyProperty
166 def parents(self):
153 def parents(self):
167 """
154 """
168 Returns list of parents changesets.
155 Returns list of parents changesets.
169 """
156 """
170 return [self.repository.get_changeset(parent)
157 return [self.repository.get_changeset(parent)
171 for parent in self._commit.parents]
158 for parent in self._commit.parents]
172
159
173 def next(self, branch=None):
160 def next(self, branch=None):
174
161
175 if branch and self.branch != branch:
162 if branch and self.branch != branch:
176 raise VCSError('Branch option used on changeset not belonging '
163 raise VCSError('Branch option used on changeset not belonging '
177 'to that branch')
164 'to that branch')
178
165
179 def _next(changeset, branch):
166 def _next(changeset, branch):
180 try:
167 try:
181 next_ = changeset.revision + 1
168 next_ = changeset.revision + 1
182 next_rev = changeset.repository.revisions[next_]
169 next_rev = changeset.repository.revisions[next_]
183 except IndexError:
170 except IndexError:
184 raise ChangesetDoesNotExistError
171 raise ChangesetDoesNotExistError
185 cs = changeset.repository.get_changeset(next_rev)
172 cs = changeset.repository.get_changeset(next_rev)
186
173
187 if branch and branch != cs.branch:
174 if branch and branch != cs.branch:
188 return _next(cs, branch)
175 return _next(cs, branch)
189
176
190 return cs
177 return cs
191
178
192 return _next(self, branch)
179 return _next(self, branch)
193
180
194 def prev(self, branch=None):
181 def prev(self, branch=None):
195 if branch and self.branch != branch:
182 if branch and self.branch != branch:
196 raise VCSError('Branch option used on changeset not belonging '
183 raise VCSError('Branch option used on changeset not belonging '
197 'to that branch')
184 'to that branch')
198
185
199 def _prev(changeset, branch):
186 def _prev(changeset, branch):
200 try:
187 try:
201 prev_ = changeset.revision - 1
188 prev_ = changeset.revision - 1
202 if prev_ < 0:
189 if prev_ < 0:
203 raise IndexError
190 raise IndexError
204 prev_rev = changeset.repository.revisions[prev_]
191 prev_rev = changeset.repository.revisions[prev_]
205 except IndexError:
192 except IndexError:
206 raise ChangesetDoesNotExistError
193 raise ChangesetDoesNotExistError
207
194
208 cs = changeset.repository.get_changeset(prev_rev)
195 cs = changeset.repository.get_changeset(prev_rev)
209
196
210 if branch and branch != cs.branch:
197 if branch and branch != cs.branch:
211 return _prev(cs, branch)
198 return _prev(cs, branch)
212
199
213 return cs
200 return cs
214
201
215 return _prev(self, branch)
202 return _prev(self, branch)
216
203
217 def get_file_mode(self, path):
204 def get_file_mode(self, path):
218 """
205 """
219 Returns stat mode of the file at the given ``path``.
206 Returns stat mode of the file at the given ``path``.
220 """
207 """
221 # ensure path is traversed
208 # ensure path is traversed
222 self._get_id_for_path(path)
209 self._get_id_for_path(path)
223 return self._stat_modes[path]
210 return self._stat_modes[path]
224
211
225 def get_file_content(self, path):
212 def get_file_content(self, path):
226 """
213 """
227 Returns content of the file at given ``path``.
214 Returns content of the file at given ``path``.
228 """
215 """
229 id = self._get_id_for_path(path)
216 id = self._get_id_for_path(path)
230 blob = self.repository._repo[id]
217 blob = self.repository._repo[id]
231 return blob.as_pretty_string()
218 return blob.as_pretty_string()
232
219
233 def get_file_size(self, path):
220 def get_file_size(self, path):
234 """
221 """
235 Returns size of the file at given ``path``.
222 Returns size of the file at given ``path``.
236 """
223 """
237 id = self._get_id_for_path(path)
224 id = self._get_id_for_path(path)
238 blob = self.repository._repo[id]
225 blob = self.repository._repo[id]
239 return blob.raw_length()
226 return blob.raw_length()
240
227
241 def get_file_changeset(self, path):
228 def get_file_changeset(self, path):
242 """
229 """
243 Returns last commit of the file at the given ``path``.
230 Returns last commit of the file at the given ``path``.
244 """
231 """
245 node = self.get_node(path)
232 node = self.get_node(path)
246 return node.history[0]
233 return node.history[0]
247
234
248 def get_file_history(self, path):
235 def get_file_history(self, path):
249 """
236 """
250 Returns history of file as reversed list of ``Changeset`` objects for
237 Returns history of file as reversed list of ``Changeset`` objects for
251 which file at given ``path`` has been modified.
238 which file at given ``path`` has been modified.
252
239
253 TODO: This function now uses os underlying 'git' and 'grep' commands
240 TODO: This function now uses os underlying 'git' and 'grep' commands
254 which is generally not good. Should be replaced with algorithm
241 which is generally not good. Should be replaced with algorithm
255 iterating commits.
242 iterating commits.
256 """
243 """
257 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
244 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
258 self.id, path
245 self.id, path
259 )
246 )
260 so, se = self.repository.run_git_command(cmd)
247 so, se = self.repository.run_git_command(cmd)
261 ids = re.findall(r'\w{40}', so)
248 ids = re.findall(r'\w{40}', so)
262 return [self.repository.get_changeset(id) for id in ids]
249 return [self.repository.get_changeset(id) for id in ids]
263
250
264 def get_file_annotate(self, path):
251 def get_file_annotate(self, path):
265 """
252 """
266 Returns a list of three element tuples with lineno,changeset and line
253 Returns a list of three element tuples with lineno,changeset and line
267
254
268 TODO: This function now uses os underlying 'git' command which is
255 TODO: This function now uses os underlying 'git' command which is
269 generally not good. Should be replaced with algorithm iterating
256 generally not good. Should be replaced with algorithm iterating
270 commits.
257 commits.
271 """
258 """
272 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
259 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
273 # -l ==> outputs long shas (and we need all 40 characters)
260 # -l ==> outputs long shas (and we need all 40 characters)
274 # --root ==> doesn't put '^' character for bounderies
261 # --root ==> doesn't put '^' character for bounderies
275 # -r sha ==> blames for the given revision
262 # -r sha ==> blames for the given revision
276 so, se = self.repository.run_git_command(cmd)
263 so, se = self.repository.run_git_command(cmd)
277 annotate = []
264 annotate = []
278 for i, blame_line in enumerate(so.split('\n')[:-1]):
265 for i, blame_line in enumerate(so.split('\n')[:-1]):
279 ln_no = i + 1
266 ln_no = i + 1
280 id, line = re.split(r' \(.+?\) ', blame_line, 1)
267 id, line = re.split(r' \(.+?\) ', blame_line, 1)
281 annotate.append((ln_no, self.repository.get_changeset(id), line))
268 annotate.append((ln_no, self.repository.get_changeset(id), line))
282 return annotate
269 return annotate
283
270
284 def fill_archive(self, stream=None, kind='tgz', prefix=None,
271 def fill_archive(self, stream=None, kind='tgz', prefix=None,
285 subrepos=False):
272 subrepos=False):
286 """
273 """
287 Fills up given stream.
274 Fills up given stream.
288
275
289 :param stream: file like object.
276 :param stream: file like object.
290 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
277 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
291 Default: ``tgz``.
278 Default: ``tgz``.
292 :param prefix: name of root directory in archive.
279 :param prefix: name of root directory in archive.
293 Default is repository name and changeset's raw_id joined with dash
280 Default is repository name and changeset's raw_id joined with dash
294 (``repo-tip.<KIND>``).
281 (``repo-tip.<KIND>``).
295 :param subrepos: include subrepos in this archive.
282 :param subrepos: include subrepos in this archive.
296
283
297 :raise ImproperArchiveTypeError: If given kind is wrong.
284 :raise ImproperArchiveTypeError: If given kind is wrong.
298 :raise VcsError: If given stream is None
285 :raise VcsError: If given stream is None
299
286
300 """
287 """
301 allowed_kinds = settings.ARCHIVE_SPECS.keys()
288 allowed_kinds = settings.ARCHIVE_SPECS.keys()
302 if kind not in allowed_kinds:
289 if kind not in allowed_kinds:
303 raise ImproperArchiveTypeError('Archive kind not supported use one'
290 raise ImproperArchiveTypeError('Archive kind not supported use one'
304 'of %s', allowed_kinds)
291 'of %s', allowed_kinds)
305
292
306 if prefix is None:
293 if prefix is None:
307 prefix = '%s-%s' % (self.repository.name, self.short_id)
294 prefix = '%s-%s' % (self.repository.name, self.short_id)
308 elif prefix.startswith('/'):
295 elif prefix.startswith('/'):
309 raise VCSError("Prefix cannot start with leading slash")
296 raise VCSError("Prefix cannot start with leading slash")
310 elif prefix.strip() == '':
297 elif prefix.strip() == '':
311 raise VCSError("Prefix cannot be empty")
298 raise VCSError("Prefix cannot be empty")
312
299
313 if kind == 'zip':
300 if kind == 'zip':
314 frmt = 'zip'
301 frmt = 'zip'
315 else:
302 else:
316 frmt = 'tar'
303 frmt = 'tar'
317 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
304 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
318 self.raw_id)
305 self.raw_id)
319 if kind == 'tgz':
306 if kind == 'tgz':
320 cmd += ' | gzip -9'
307 cmd += ' | gzip -9'
321 elif kind == 'tbz2':
308 elif kind == 'tbz2':
322 cmd += ' | bzip2 -9'
309 cmd += ' | bzip2 -9'
323
310
324 if stream is None:
311 if stream is None:
325 raise VCSError('You need to pass in a valid stream for filling'
312 raise VCSError('You need to pass in a valid stream for filling'
326 ' with archival data')
313 ' with archival data')
327 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
314 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
328 cwd=self.repository.path)
315 cwd=self.repository.path)
329
316
330 buffer_size = 1024 * 8
317 buffer_size = 1024 * 8
331 chunk = popen.stdout.read(buffer_size)
318 chunk = popen.stdout.read(buffer_size)
332 while chunk:
319 while chunk:
333 stream.write(chunk)
320 stream.write(chunk)
334 chunk = popen.stdout.read(buffer_size)
321 chunk = popen.stdout.read(buffer_size)
335 # Make sure all descriptors would be read
322 # Make sure all descriptors would be read
336 popen.communicate()
323 popen.communicate()
337
324
338 def get_nodes(self, path):
325 def get_nodes(self, path):
339 if self._get_kind(path) != NodeKind.DIR:
326 if self._get_kind(path) != NodeKind.DIR:
340 raise ChangesetError("Directory does not exist for revision %r at "
327 raise ChangesetError("Directory does not exist for revision %r at "
341 " %r" % (self.revision, path))
328 " %r" % (self.revision, path))
342 path = self._fix_path(path)
329 path = self._fix_path(path)
343 id = self._get_id_for_path(path)
330 id = self._get_id_for_path(path)
344 tree = self.repository._repo[id]
331 tree = self.repository._repo[id]
345 dirnodes = []
332 dirnodes = []
346 filenodes = []
333 filenodes = []
347 for name, stat, id in tree.iteritems():
334 for name, stat, id in tree.iteritems():
348 obj = self.repository._repo.get_object(id)
335 obj = self.repository._repo.get_object(id)
349 if path != '':
336 if path != '':
350 obj_path = '/'.join((path, name))
337 obj_path = '/'.join((path, name))
351 else:
338 else:
352 obj_path = name
339 obj_path = name
353 if obj_path not in self._stat_modes:
340 if obj_path not in self._stat_modes:
354 self._stat_modes[obj_path] = stat
341 self._stat_modes[obj_path] = stat
355 if isinstance(obj, objects.Tree):
342 if isinstance(obj, objects.Tree):
356 dirnodes.append(DirNode(obj_path, changeset=self))
343 dirnodes.append(DirNode(obj_path, changeset=self))
357 elif isinstance(obj, objects.Blob):
344 elif isinstance(obj, objects.Blob):
358 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
345 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
359 else:
346 else:
360 raise ChangesetError("Requested object should be Tree "
347 raise ChangesetError("Requested object should be Tree "
361 "or Blob, is %r" % type(obj))
348 "or Blob, is %r" % type(obj))
362 nodes = dirnodes + filenodes
349 nodes = dirnodes + filenodes
363 for node in nodes:
350 for node in nodes:
364 if not node.path in self.nodes:
351 if not node.path in self.nodes:
365 self.nodes[node.path] = node
352 self.nodes[node.path] = node
366 nodes.sort()
353 nodes.sort()
367 return nodes
354 return nodes
368
355
369 def get_node(self, path):
356 def get_node(self, path):
370 if isinstance(path, unicode):
357 if isinstance(path, unicode):
371 path = path.encode('utf-8')
358 path = path.encode('utf-8')
372 path = self._fix_path(path)
359 path = self._fix_path(path)
373 if not path in self.nodes:
360 if not path in self.nodes:
374 try:
361 try:
375 id = self._get_id_for_path(path)
362 id = self._get_id_for_path(path)
376 except ChangesetError:
363 except ChangesetError:
377 raise NodeDoesNotExistError("Cannot find one of parents' "
364 raise NodeDoesNotExistError("Cannot find one of parents' "
378 "directories for a given path: %s" % path)
365 "directories for a given path: %s" % path)
379 obj = self.repository._repo.get_object(id)
366 obj = self.repository._repo.get_object(id)
380 if isinstance(obj, objects.Tree):
367 if isinstance(obj, objects.Tree):
381 if path == '':
368 if path == '':
382 node = RootNode(changeset=self)
369 node = RootNode(changeset=self)
383 else:
370 else:
384 node = DirNode(path, changeset=self)
371 node = DirNode(path, changeset=self)
385 node._tree = obj
372 node._tree = obj
386 elif isinstance(obj, objects.Blob):
373 elif isinstance(obj, objects.Blob):
387 node = FileNode(path, changeset=self)
374 node = FileNode(path, changeset=self)
388 node._blob = obj
375 node._blob = obj
389 else:
376 else:
390 raise NodeDoesNotExistError("There is no file nor directory "
377 raise NodeDoesNotExistError("There is no file nor directory "
391 "at the given path %r at revision %r"
378 "at the given path %r at revision %r"
392 % (path, self.short_id))
379 % (path, self.short_id))
393 # cache node
380 # cache node
394 self.nodes[path] = node
381 self.nodes[path] = node
395 return self.nodes[path]
382 return self.nodes[path]
396
383
397 @LazyProperty
384 @LazyProperty
398 def affected_files(self):
385 def affected_files(self):
399 """
386 """
400 Get's a fast accessible file changes for given changeset
387 Get's a fast accessible file changes for given changeset
401 """
388 """
402
389
403 return self.added + self.changed
390 return self.added + self.changed
404
391
405 @LazyProperty
392 @LazyProperty
406 def _diff_name_status(self):
393 def _diff_name_status(self):
407 output = []
394 output = []
408 for parent in self.parents:
395 for parent in self.parents:
409 cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
396 cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
410 so, se = self.repository.run_git_command(cmd)
397 so, se = self.repository.run_git_command(cmd)
411 output.append(so.strip())
398 output.append(so.strip())
412 return '\n'.join(output)
399 return '\n'.join(output)
413
400
414 def _get_paths_for_status(self, status):
401 def _get_paths_for_status(self, status):
415 """
402 """
416 Returns sorted list of paths for given ``status``.
403 Returns sorted list of paths for given ``status``.
417
404
418 :param status: one of: *added*, *modified* or *deleted*
405 :param status: one of: *added*, *modified* or *deleted*
419 """
406 """
420 paths = set()
407 paths = set()
421 char = status[0].upper()
408 char = status[0].upper()
422 for line in self._diff_name_status.splitlines():
409 for line in self._diff_name_status.splitlines():
423 if not line:
410 if not line:
424 continue
411 continue
425 if line.startswith(char):
412 if line.startswith(char):
426 splitted = line.split(char,1)
413 splitted = line.split(char,1)
427 if not len(splitted) == 2:
414 if not len(splitted) == 2:
428 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
415 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
429 "particularly that line: %s" % (self._diff_name_status,
416 "particularly that line: %s" % (self._diff_name_status,
430 line))
417 line))
431 paths.add(splitted[1].strip())
418 paths.add(splitted[1].strip())
432 return sorted(paths)
419 return sorted(paths)
433
420
434 @LazyProperty
421 @LazyProperty
435 def added(self):
422 def added(self):
436 """
423 """
437 Returns list of added ``FileNode`` objects.
424 Returns list of added ``FileNode`` objects.
438 """
425 """
439 if not self.parents:
426 if not self.parents:
440 return list(self._get_file_nodes())
427 return list(self._get_file_nodes())
441 return [self.get_node(path) for path in self._get_paths_for_status('added')]
428 return [self.get_node(path) for path in self._get_paths_for_status('added')]
442
429
443 @LazyProperty
430 @LazyProperty
444 def changed(self):
431 def changed(self):
445 """
432 """
446 Returns list of modified ``FileNode`` objects.
433 Returns list of modified ``FileNode`` objects.
447 """
434 """
448 if not self.parents:
435 if not self.parents:
449 return []
436 return []
450 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
437 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
451
438
452 @LazyProperty
439 @LazyProperty
453 def removed(self):
440 def removed(self):
454 """
441 """
455 Returns list of removed ``FileNode`` objects.
442 Returns list of removed ``FileNode`` objects.
456 """
443 """
457 if not self.parents:
444 if not self.parents:
458 return []
445 return []
459 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
446 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,508 +1,521
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 #cmd = '(cd %s && git %s)' % (self.path, cmd)
83 if isinstance(cmd, basestring):
83 if isinstance(cmd, basestring):
84 cmd = 'GIT_CONFIG_NOGLOBAL=1 git %s' % cmd
84 cmd = 'GIT_CONFIG_NOGLOBAL=1 git %s' % cmd
85 else:
85 else:
86 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + cmd
86 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + cmd
87 try:
87 try:
88 opts = dict(
88 opts = dict(
89 shell=isinstance(cmd, basestring),
89 shell=isinstance(cmd, basestring),
90 stdout=PIPE,
90 stdout=PIPE,
91 stderr=PIPE)
91 stderr=PIPE)
92 if os.path.isdir(self.path):
92 if os.path.isdir(self.path):
93 opts['cwd'] = self.path
93 opts['cwd'] = self.path
94 p = Popen(cmd, **opts)
94 p = Popen(cmd, **opts)
95 except OSError, err:
95 except OSError, err:
96 raise RepositoryError("Couldn't run git command (%s).\n"
96 raise RepositoryError("Couldn't run git command (%s).\n"
97 "Original error was:%s" % (cmd, err))
97 "Original error was:%s" % (cmd, err))
98 so, se = p.communicate()
98 so, se = p.communicate()
99 if not se.startswith("fatal: bad default revision 'HEAD'") and \
99 if not se.startswith("fatal: bad default revision 'HEAD'") and \
100 p.returncode != 0:
100 p.returncode != 0:
101 raise RepositoryError("Couldn't run git command (%s).\n"
101 raise RepositoryError("Couldn't run git command (%s).\n"
102 "stderr:\n%s" % (cmd, se))
102 "stderr:\n%s" % (cmd, se))
103 return so, se
103 return so, se
104
104
105 def _check_url(self, url):
105 def _check_url(self, url):
106 """
106 """
107 Functon will check given url and try to verify if it's a valid
107 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
108 link. Sometimes it may happened that mercurial will issue basic
109 auth request that can cause whole API to hang when used from python
109 auth request that can cause whole API to hang when used from python
110 or other external calls.
110 or other external calls.
111
111
112 On failures it'll raise urllib2.HTTPError
112 On failures it'll raise urllib2.HTTPError
113 """
113 """
114
114
115 #TODO: implement this
115 #TODO: implement this
116 pass
116 pass
117
117
118 def _get_repo(self, create, src_url=None, update_after_clone=False,
118 def _get_repo(self, create, src_url=None, update_after_clone=False,
119 bare=False):
119 bare=False):
120 if create and os.path.exists(self.path):
120 if create and os.path.exists(self.path):
121 raise RepositoryError("Location already exist")
121 raise RepositoryError("Location already exist")
122 if src_url and not create:
122 if src_url and not create:
123 raise RepositoryError("Create should be set to True if src_url is "
123 raise RepositoryError("Create should be set to True if src_url is "
124 "given (clone operation creates repository)")
124 "given (clone operation creates repository)")
125 try:
125 try:
126 if create and src_url:
126 if create and src_url:
127 self._check_url(src_url)
127 self._check_url(src_url)
128 self.clone(src_url, update_after_clone, bare)
128 self.clone(src_url, update_after_clone, bare)
129 return Repo(self.path)
129 return Repo(self.path)
130 elif create:
130 elif create:
131 os.mkdir(self.path)
131 os.mkdir(self.path)
132 if bare:
132 if bare:
133 return Repo.init_bare(self.path)
133 return Repo.init_bare(self.path)
134 else:
134 else:
135 return Repo.init(self.path)
135 return Repo.init(self.path)
136 else:
136 else:
137 return Repo(self.path)
137 return Repo(self.path)
138 except (NotGitRepository, OSError), err:
138 except (NotGitRepository, OSError), err:
139 raise RepositoryError(err)
139 raise RepositoryError(err)
140
140
141 def _get_all_revisions(self):
141 def _get_all_revisions(self):
142 cmd = 'rev-list --all --date-order'
142 cmd = 'rev-list --all --date-order'
143 try:
143 try:
144 so, se = self.run_git_command(cmd)
144 so, se = self.run_git_command(cmd)
145 except RepositoryError:
145 except RepositoryError:
146 # Can be raised for empty repositories
146 # Can be raised for empty repositories
147 return []
147 return []
148 revisions = so.splitlines()
148 revisions = so.splitlines()
149 revisions.reverse()
149 revisions.reverse()
150 return revisions
150 return revisions
151
151
152 def _get_revision(self, revision):
152 def _get_revision(self, revision):
153 """
153 """
154 For git backend we always return integer here. This way we ensure
154 For git backend we always return integer here. This way we ensure
155 that changset's revision attribute would become integer.
155 that changset's revision attribute would become integer.
156 """
156 """
157 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
157 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
158 is_bstr = lambda o: isinstance(o, (str, unicode))
158 is_bstr = lambda o: isinstance(o, (str, unicode))
159 is_null = lambda o: len(o) == revision.count('0')
159 is_null = lambda o: len(o) == revision.count('0')
160
160
161 if len(self.revisions) == 0:
161 if len(self.revisions) == 0:
162 raise EmptyRepositoryError("There are no changesets yet")
162 raise EmptyRepositoryError("There are no changesets yet")
163
163
164 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
164 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
165 revision = self.revisions[-1]
165 revision = self.revisions[-1]
166
166
167 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
167 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
168 or isinstance(revision, int) or is_null(revision)):
168 or isinstance(revision, int) or is_null(revision)):
169 try:
169 try:
170 revision = self.revisions[int(revision)]
170 revision = self.revisions[int(revision)]
171 except:
171 except:
172 raise ChangesetDoesNotExistError("Revision %r does not exist "
172 raise ChangesetDoesNotExistError("Revision %r does not exist "
173 "for this repository %s" % (revision, self))
173 "for this repository %s" % (revision, self))
174
174
175 elif is_bstr(revision):
175 elif is_bstr(revision):
176 if not pattern.match(revision) or revision not in self.revisions:
176 if not pattern.match(revision) or revision not in self.revisions:
177 raise ChangesetDoesNotExistError("Revision %r does not exist "
177 raise ChangesetDoesNotExistError("Revision %r does not exist "
178 "for this repository %s" % (revision, self))
178 "for this repository %s" % (revision, self))
179
179
180 # Ensure we return full id
180 # Ensure we return full id
181 if not pattern.match(str(revision)):
181 if not pattern.match(str(revision)):
182 raise ChangesetDoesNotExistError("Given revision %r not recognized"
182 raise ChangesetDoesNotExistError("Given revision %r not recognized"
183 % revision)
183 % revision)
184 return revision
184 return revision
185
185
186 def _get_archives(self, archive_name='tip'):
186 def _get_archives(self, archive_name='tip'):
187
187
188 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
188 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
189 yield {"type": i[0], "extension": i[1], "node": archive_name}
189 yield {"type": i[0], "extension": i[1], "node": archive_name}
190
190
191 def _get_url(self, url):
191 def _get_url(self, url):
192 """
192 """
193 Returns normalized url. If schema is not given, would fall to
193 Returns normalized url. If schema is not given, would fall to
194 filesystem (``file:///``) schema.
194 filesystem (``file:///``) schema.
195 """
195 """
196 url = str(url)
196 url = str(url)
197 if url != 'default' and not '://' in url:
197 if url != 'default' and not '://' in url:
198 url = ':///'.join(('file', url))
198 url = ':///'.join(('file', url))
199 return url
199 return url
200
200
201 @LazyProperty
201 @LazyProperty
202 def name(self):
202 def name(self):
203 return os.path.basename(self.path)
203 return os.path.basename(self.path)
204
204
205 @LazyProperty
205 @LazyProperty
206 def last_change(self):
206 def last_change(self):
207 """
207 """
208 Returns last change made on this repository as datetime object
208 Returns last change made on this repository as datetime object
209 """
209 """
210 return date_fromtimestamp(self._get_mtime(), makedate()[1])
210 return date_fromtimestamp(self._get_mtime(), makedate()[1])
211
211
212 def _get_mtime(self):
212 def _get_mtime(self):
213 try:
213 try:
214 return time.mktime(self.get_changeset().date.timetuple())
214 return time.mktime(self.get_changeset().date.timetuple())
215 except RepositoryError:
215 except RepositoryError:
216 # fallback to filesystem
216 # fallback to filesystem
217 in_path = os.path.join(self.path, '.git', "index")
217 in_path = os.path.join(self.path, '.git', "index")
218 he_path = os.path.join(self.path, '.git', "HEAD")
218 he_path = os.path.join(self.path, '.git', "HEAD")
219 if os.path.exists(in_path):
219 if os.path.exists(in_path):
220 return os.stat(in_path).st_mtime
220 return os.stat(in_path).st_mtime
221 else:
221 else:
222 return os.stat(he_path).st_mtime
222 return os.stat(he_path).st_mtime
223
223
224 @LazyProperty
224 @LazyProperty
225 def description(self):
225 def description(self):
226 undefined_description = u'unknown'
226 undefined_description = u'unknown'
227 description_path = os.path.join(self.path, '.git', 'description')
227 description_path = os.path.join(self.path, '.git', 'description')
228 if os.path.isfile(description_path):
228 if os.path.isfile(description_path):
229 return safe_unicode(open(description_path).read())
229 return safe_unicode(open(description_path).read())
230 else:
230 else:
231 return undefined_description
231 return undefined_description
232
232
233 @LazyProperty
233 @LazyProperty
234 def contact(self):
234 def contact(self):
235 undefined_contact = u'Unknown'
235 undefined_contact = u'Unknown'
236 return undefined_contact
236 return undefined_contact
237
237
238 @property
238 @property
239 def branches(self):
239 def branches(self):
240 if not self.revisions:
240 if not self.revisions:
241 return {}
241 return {}
242 refs = self._repo.refs.as_dict()
242 refs = self._repo.refs.as_dict()
243 sortkey = lambda ctx: ctx[0]
243 sortkey = lambda ctx: ctx[0]
244 _branches = [('/'.join(ref.split('/')[2:]), head)
244 _branches = [('/'.join(ref.split('/')[2:]), head)
245 for ref, head in refs.items()
245 for ref, head in refs.items()
246 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
246 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
247 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
247 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
248
248
249 def _heads(self, reverse=False):
250 refs = self._repo.get_refs()
251 heads = {}
252
253 for key, val in refs.items():
254 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
255 if key.startswith(ref_key):
256 n = key[len(ref_key):]
257 if n not in ['HEAD']:
258 heads[n] = val
259
260 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
261
249 def _get_tags(self):
262 def _get_tags(self):
250 if not self.revisions:
263 if not self.revisions:
251 return {}
264 return {}
252 sortkey = lambda ctx: ctx[0]
265 sortkey = lambda ctx: ctx[0]
253 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
266 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
254 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
267 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
255 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
268 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
256
269
257 @LazyProperty
270 @LazyProperty
258 def tags(self):
271 def tags(self):
259 return self._get_tags()
272 return self._get_tags()
260
273
261 def tag(self, name, user, revision=None, message=None, date=None,
274 def tag(self, name, user, revision=None, message=None, date=None,
262 **kwargs):
275 **kwargs):
263 """
276 """
264 Creates and returns a tag for the given ``revision``.
277 Creates and returns a tag for the given ``revision``.
265
278
266 :param name: name for new tag
279 :param name: name for new tag
267 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
280 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
268 :param revision: changeset id for which new tag would be created
281 :param revision: changeset id for which new tag would be created
269 :param message: message of the tag's commit
282 :param message: message of the tag's commit
270 :param date: date of tag's commit
283 :param date: date of tag's commit
271
284
272 :raises TagAlreadyExistError: if tag with same name already exists
285 :raises TagAlreadyExistError: if tag with same name already exists
273 """
286 """
274 if name in self.tags:
287 if name in self.tags:
275 raise TagAlreadyExistError("Tag %s already exists" % name)
288 raise TagAlreadyExistError("Tag %s already exists" % name)
276 changeset = self.get_changeset(revision)
289 changeset = self.get_changeset(revision)
277 message = message or "Added tag %s for commit %s" % (name,
290 message = message or "Added tag %s for commit %s" % (name,
278 changeset.raw_id)
291 changeset.raw_id)
279 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
292 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
280
293
281 self.tags = self._get_tags()
294 self.tags = self._get_tags()
282 return changeset
295 return changeset
283
296
284 def remove_tag(self, name, user, message=None, date=None):
297 def remove_tag(self, name, user, message=None, date=None):
285 """
298 """
286 Removes tag with the given ``name``.
299 Removes tag with the given ``name``.
287
300
288 :param name: name of the tag to be removed
301 :param name: name of the tag to be removed
289 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
302 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
290 :param message: message of the tag's removal commit
303 :param message: message of the tag's removal commit
291 :param date: date of tag's removal commit
304 :param date: date of tag's removal commit
292
305
293 :raises TagDoesNotExistError: if tag with given name does not exists
306 :raises TagDoesNotExistError: if tag with given name does not exists
294 """
307 """
295 if name not in self.tags:
308 if name not in self.tags:
296 raise TagDoesNotExistError("Tag %s does not exist" % name)
309 raise TagDoesNotExistError("Tag %s does not exist" % name)
297 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
310 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
298 try:
311 try:
299 os.remove(tagpath)
312 os.remove(tagpath)
300 self.tags = self._get_tags()
313 self.tags = self._get_tags()
301 except OSError, e:
314 except OSError, e:
302 raise RepositoryError(e.strerror)
315 raise RepositoryError(e.strerror)
303
316
304 def get_changeset(self, revision=None):
317 def get_changeset(self, revision=None):
305 """
318 """
306 Returns ``GitChangeset`` object representing commit from git repository
319 Returns ``GitChangeset`` object representing commit from git repository
307 at the given revision or head (most recent commit) if None given.
320 at the given revision or head (most recent commit) if None given.
308 """
321 """
309 if isinstance(revision, GitChangeset):
322 if isinstance(revision, GitChangeset):
310 return revision
323 return revision
311 revision = self._get_revision(revision)
324 revision = self._get_revision(revision)
312 changeset = GitChangeset(repository=self, revision=revision)
325 changeset = GitChangeset(repository=self, revision=revision)
313 return changeset
326 return changeset
314
327
315 def get_changesets(self, start=None, end=None, start_date=None,
328 def get_changesets(self, start=None, end=None, start_date=None,
316 end_date=None, branch_name=None, reverse=False):
329 end_date=None, branch_name=None, reverse=False):
317 """
330 """
318 Returns iterator of ``GitChangeset`` objects from start to end (both
331 Returns iterator of ``GitChangeset`` objects from start to end (both
319 are inclusive), in ascending date order (unless ``reverse`` is set).
332 are inclusive), in ascending date order (unless ``reverse`` is set).
320
333
321 :param start: changeset ID, as str; first returned changeset
334 :param start: changeset ID, as str; first returned changeset
322 :param end: changeset ID, as str; last returned changeset
335 :param end: changeset ID, as str; last returned changeset
323 :param start_date: if specified, changesets with commit date less than
336 :param start_date: if specified, changesets with commit date less than
324 ``start_date`` would be filtered out from returned set
337 ``start_date`` would be filtered out from returned set
325 :param end_date: if specified, changesets with commit date greater than
338 :param end_date: if specified, changesets with commit date greater than
326 ``end_date`` would be filtered out from returned set
339 ``end_date`` would be filtered out from returned set
327 :param branch_name: if specified, changesets not reachable from given
340 :param branch_name: if specified, changesets not reachable from given
328 branch would be filtered out from returned set
341 branch would be filtered out from returned set
329 :param reverse: if ``True``, returned generator would be reversed
342 :param reverse: if ``True``, returned generator would be reversed
330 (meaning that returned changesets would have descending date order)
343 (meaning that returned changesets would have descending date order)
331
344
332 :raise BranchDoesNotExistError: If given ``branch_name`` does not
345 :raise BranchDoesNotExistError: If given ``branch_name`` does not
333 exist.
346 exist.
334 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
347 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
335 ``end`` could not be found.
348 ``end`` could not be found.
336
349
337 """
350 """
338 if branch_name and branch_name not in self.branches:
351 if branch_name and branch_name not in self.branches:
339 raise BranchDoesNotExistError("Branch '%s' not found" \
352 raise BranchDoesNotExistError("Branch '%s' not found" \
340 % branch_name)
353 % branch_name)
341 # %H at format means (full) commit hash, initial hashes are retrieved
354 # %H at format means (full) commit hash, initial hashes are retrieved
342 # in ascending date order
355 # in ascending date order
343 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
356 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
344 cmd_params = {}
357 cmd_params = {}
345 if start_date:
358 if start_date:
346 cmd_template += ' --since "$since"'
359 cmd_template += ' --since "$since"'
347 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
360 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
348 if end_date:
361 if end_date:
349 cmd_template += ' --until "$until"'
362 cmd_template += ' --until "$until"'
350 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
363 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
351 if branch_name:
364 if branch_name:
352 cmd_template += ' $branch_name'
365 cmd_template += ' $branch_name'
353 cmd_params['branch_name'] = branch_name
366 cmd_params['branch_name'] = branch_name
354 else:
367 else:
355 cmd_template += ' --all'
368 cmd_template += ' --all'
356
369
357 cmd = Template(cmd_template).safe_substitute(**cmd_params)
370 cmd = Template(cmd_template).safe_substitute(**cmd_params)
358 revs = self.run_git_command(cmd)[0].splitlines()
371 revs = self.run_git_command(cmd)[0].splitlines()
359 start_pos = 0
372 start_pos = 0
360 end_pos = len(revs)
373 end_pos = len(revs)
361 if start:
374 if start:
362 _start = self._get_revision(start)
375 _start = self._get_revision(start)
363 try:
376 try:
364 start_pos = revs.index(_start)
377 start_pos = revs.index(_start)
365 except ValueError:
378 except ValueError:
366 pass
379 pass
367
380
368 if end is not None:
381 if end is not None:
369 _end = self._get_revision(end)
382 _end = self._get_revision(end)
370 try:
383 try:
371 end_pos = revs.index(_end)
384 end_pos = revs.index(_end)
372 except ValueError:
385 except ValueError:
373 pass
386 pass
374
387
375 if None not in [start, end] and start_pos > end_pos:
388 if None not in [start, end] and start_pos > end_pos:
376 raise RepositoryError('start cannot be after end')
389 raise RepositoryError('start cannot be after end')
377
390
378 if end_pos is not None:
391 if end_pos is not None:
379 end_pos += 1
392 end_pos += 1
380
393
381 revs = revs[start_pos:end_pos]
394 revs = revs[start_pos:end_pos]
382 if reverse:
395 if reverse:
383 revs = reversed(revs)
396 revs = reversed(revs)
384 for rev in revs:
397 for rev in revs:
385 yield self.get_changeset(rev)
398 yield self.get_changeset(rev)
386
399
387 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
400 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
388 context=3):
401 context=3):
389 """
402 """
390 Returns (git like) *diff*, as plain text. Shows changes introduced by
403 Returns (git like) *diff*, as plain text. Shows changes introduced by
391 ``rev2`` since ``rev1``.
404 ``rev2`` since ``rev1``.
392
405
393 :param rev1: Entry point from which diff is shown. Can be
406 :param rev1: Entry point from which diff is shown. Can be
394 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
407 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
395 the changes since empty state of the repository until ``rev2``
408 the changes since empty state of the repository until ``rev2``
396 :param rev2: Until which revision changes should be shown.
409 :param rev2: Until which revision changes should be shown.
397 :param ignore_whitespace: If set to ``True``, would not show whitespace
410 :param ignore_whitespace: If set to ``True``, would not show whitespace
398 changes. Defaults to ``False``.
411 changes. Defaults to ``False``.
399 :param context: How many lines before/after changed lines should be
412 :param context: How many lines before/after changed lines should be
400 shown. Defaults to ``3``.
413 shown. Defaults to ``3``.
401 """
414 """
402 flags = ['-U%s' % context]
415 flags = ['-U%s' % context]
403 if ignore_whitespace:
416 if ignore_whitespace:
404 flags.append('-w')
417 flags.append('-w')
405
418
406 if rev1 == self.EMPTY_CHANGESET:
419 if rev1 == self.EMPTY_CHANGESET:
407 rev2 = self.get_changeset(rev2).raw_id
420 rev2 = self.get_changeset(rev2).raw_id
408 cmd = ' '.join(['show'] + flags + [rev2])
421 cmd = ' '.join(['show'] + flags + [rev2])
409 else:
422 else:
410 rev1 = self.get_changeset(rev1).raw_id
423 rev1 = self.get_changeset(rev1).raw_id
411 rev2 = self.get_changeset(rev2).raw_id
424 rev2 = self.get_changeset(rev2).raw_id
412 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
425 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
413
426
414 if path:
427 if path:
415 cmd += ' -- "%s"' % path
428 cmd += ' -- "%s"' % path
416 stdout, stderr = self.run_git_command(cmd)
429 stdout, stderr = self.run_git_command(cmd)
417 # If we used 'show' command, strip first few lines (until actual diff
430 # If we used 'show' command, strip first few lines (until actual diff
418 # starts)
431 # starts)
419 if rev1 == self.EMPTY_CHANGESET:
432 if rev1 == self.EMPTY_CHANGESET:
420 lines = stdout.splitlines()
433 lines = stdout.splitlines()
421 x = 0
434 x = 0
422 for line in lines:
435 for line in lines:
423 if line.startswith('diff'):
436 if line.startswith('diff'):
424 break
437 break
425 x += 1
438 x += 1
426 # Append new line just like 'diff' command do
439 # Append new line just like 'diff' command do
427 stdout = '\n'.join(lines[x:]) + '\n'
440 stdout = '\n'.join(lines[x:]) + '\n'
428 return stdout
441 return stdout
429
442
430 @LazyProperty
443 @LazyProperty
431 def in_memory_changeset(self):
444 def in_memory_changeset(self):
432 """
445 """
433 Returns ``GitInMemoryChangeset`` object for this repository.
446 Returns ``GitInMemoryChangeset`` object for this repository.
434 """
447 """
435 return GitInMemoryChangeset(self)
448 return GitInMemoryChangeset(self)
436
449
437 def clone(self, url, update_after_clone=True, bare=False):
450 def clone(self, url, update_after_clone=True, bare=False):
438 """
451 """
439 Tries to clone changes from external location.
452 Tries to clone changes from external location.
440
453
441 :param update_after_clone: If set to ``False``, git won't checkout
454 :param update_after_clone: If set to ``False``, git won't checkout
442 working directory
455 working directory
443 :param bare: If set to ``True``, repository would be cloned into
456 :param bare: If set to ``True``, repository would be cloned into
444 *bare* git repository (no working directory at all).
457 *bare* git repository (no working directory at all).
445 """
458 """
446 url = self._get_url(url)
459 url = self._get_url(url)
447 cmd = ['clone']
460 cmd = ['clone']
448 if bare:
461 if bare:
449 cmd.append('--bare')
462 cmd.append('--bare')
450 elif not update_after_clone:
463 elif not update_after_clone:
451 cmd.append('--no-checkout')
464 cmd.append('--no-checkout')
452 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
465 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
453 cmd = ' '.join(cmd)
466 cmd = ' '.join(cmd)
454 # If error occurs run_git_command raises RepositoryError already
467 # If error occurs run_git_command raises RepositoryError already
455 self.run_git_command(cmd)
468 self.run_git_command(cmd)
456
469
457 @LazyProperty
470 @LazyProperty
458 def workdir(self):
471 def workdir(self):
459 """
472 """
460 Returns ``Workdir`` instance for this repository.
473 Returns ``Workdir`` instance for this repository.
461 """
474 """
462 return GitWorkdir(self)
475 return GitWorkdir(self)
463
476
464 def get_config_value(self, section, name, config_file=None):
477 def get_config_value(self, section, name, config_file=None):
465 """
478 """
466 Returns configuration value for a given [``section``] and ``name``.
479 Returns configuration value for a given [``section``] and ``name``.
467
480
468 :param section: Section we want to retrieve value from
481 :param section: Section we want to retrieve value from
469 :param name: Name of configuration we want to retrieve
482 :param name: Name of configuration we want to retrieve
470 :param config_file: A path to file which should be used to retrieve
483 :param config_file: A path to file which should be used to retrieve
471 configuration from (might also be a list of file paths)
484 configuration from (might also be a list of file paths)
472 """
485 """
473 if config_file is None:
486 if config_file is None:
474 config_file = []
487 config_file = []
475 elif isinstance(config_file, basestring):
488 elif isinstance(config_file, basestring):
476 config_file = [config_file]
489 config_file = [config_file]
477
490
478 def gen_configs():
491 def gen_configs():
479 for path in config_file + self._config_files:
492 for path in config_file + self._config_files:
480 try:
493 try:
481 yield ConfigFile.from_path(path)
494 yield ConfigFile.from_path(path)
482 except (IOError, OSError, ValueError):
495 except (IOError, OSError, ValueError):
483 continue
496 continue
484
497
485 for config in gen_configs():
498 for config in gen_configs():
486 try:
499 try:
487 return config.get(section, name)
500 return config.get(section, name)
488 except KeyError:
501 except KeyError:
489 continue
502 continue
490 return None
503 return None
491
504
492 def get_user_name(self, config_file=None):
505 def get_user_name(self, config_file=None):
493 """
506 """
494 Returns user's name from global configuration file.
507 Returns user's name from global configuration file.
495
508
496 :param config_file: A path to file which should be used to retrieve
509 :param config_file: A path to file which should be used to retrieve
497 configuration from (might also be a list of file paths)
510 configuration from (might also be a list of file paths)
498 """
511 """
499 return self.get_config_value('user', 'name', config_file)
512 return self.get_config_value('user', 'name', config_file)
500
513
501 def get_user_email(self, config_file=None):
514 def get_user_email(self, config_file=None):
502 """
515 """
503 Returns user's email from global configuration file.
516 Returns user's email from global configuration file.
504
517
505 :param config_file: A path to file which should be used to retrieve
518 :param config_file: A path to file which should be used to retrieve
506 configuration from (might also be a list of file paths)
519 configuration from (might also be a list of file paths)
507 """
520 """
508 return self.get_config_value('user', 'email', config_file)
521 return self.get_config_value('user', 'email', config_file)
@@ -1,230 +1,230
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
7 </%def>
7 </%def>
8
8
9 <%def name="breadcrumbs_links()">
9 <%def name="breadcrumbs_links()">
10 ${h.link_to(u'Home',h.url('/'))}
10 ${h.link_to(u'Home',h.url('/'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 &raquo;
13 &raquo;
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 </%def>
15 </%def>
16
16
17 <%def name="page_nav()">
17 <%def name="page_nav()">
18 ${self.menu('changelog')}
18 ${self.menu('changelog')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <div class="table">
27 <div class="table">
28 % if c.pagination:
28 % if c.pagination:
29 <div id="graph">
29 <div id="graph">
30 <div id="graph_nodes">
30 <div id="graph_nodes">
31 <canvas id="graph_canvas"></canvas>
31 <canvas id="graph_canvas"></canvas>
32 </div>
32 </div>
33 <div id="graph_content">
33 <div id="graph_content">
34 <div class="container_header">
34 <div class="container_header">
35 ${h.form(h.url.current(),method='get')}
35 ${h.form(h.url.current(),method='get')}
36 <div class="info_box" style="float:left">
36 <div class="info_box" style="float:left">
37 ${h.submit('set',_('Show'),class_="ui-btn")}
37 ${h.submit('set',_('Show'),class_="ui-btn")}
38 ${h.text('size',size=1,value=c.size)}
38 ${h.text('size',size=1,value=c.size)}
39 ${_('revisions')}
39 ${_('revisions')}
40 </div>
40 </div>
41 ${h.end_form()}
41 ${h.end_form()}
42 <div id="rev_range_container" style="display:none"></div>
42 <div id="rev_range_container" style="display:none"></div>
43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
44 </div>
44 </div>
45
45
46 %for cnt,cs in enumerate(c.pagination):
46 %for cnt,cs in enumerate(c.pagination):
47 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
47 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
48 <div class="left">
48 <div class="left">
49 <div>
49 <div>
50 ${h.checkbox(cs.short_id,class_="changeset_range")}
50 ${h.checkbox(cs.short_id,class_="changeset_range")}
51 <span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
51 <span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
52 </div>
52 </div>
53 <div class="author">
53 <div class="author">
54 <div class="gravatar">
54 <div class="gravatar">
55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
56 </div>
56 </div>
57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
58 </div>
58 </div>
59 <div class="date">${cs.date}</div>
59 <div class="date">${cs.date}</div>
60 </div>
60 </div>
61 <div class="mid">
61 <div class="mid">
62 <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
62 <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
63 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
63 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
64 </div>
64 </div>
65 <div class="right">
65 <div class="right">
66 <div id="${cs.raw_id}_changes_info" class="changes">
66 <div id="${cs.raw_id}_changes_info" class="changes">
67 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
67 <div id="${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
68 <div class="comments-container">
68 <div class="comments-container">
69 %if len(c.comments.get(cs.raw_id,[])) > 0:
69 %if len(c.comments.get(cs.raw_id,[])) > 0:
70 <div class="comments-cnt" title="${('comments')}">
70 <div class="comments-cnt" title="${('comments')}">
71 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
71 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
72 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
72 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
73 <img src="${h.url('/images/icons/comments.png')}">
73 <img src="${h.url('/images/icons/comments.png')}">
74 </a>
74 </a>
75 </div>
75 </div>
76 %endif
76 %endif
77 </div>
77 </div>
78 </div>
78 </div>
79 %if cs.parents:
79 %if cs.parents:
80 %for p_cs in reversed(cs.parents):
80 %for p_cs in reversed(cs.parents):
81 <div class="parent">${_('Parent')}
81 <div class="parent">${_('Parent')}
82 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
82 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
83 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
83 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
84 </div>
84 </div>
85 %endfor
85 %endfor
86 %else:
86 %else:
87 <div class="parent">${_('No parents')}</div>
87 <div class="parent">${_('No parents')}</div>
88 %endif
88 %endif
89
89
90 <span class="logtags">
90 <span class="logtags">
91 %if len(cs.parents)>1:
91 %if len(cs.parents)>1:
92 <span class="merge">${_('merge')}</span>
92 <span class="merge">${_('merge')}</span>
93 %endif
93 %endif
94 %if h.is_hg(c.rhodecode_repo) and cs.branch:
94 %if cs.branch:
95 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
95 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
96 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
96 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
97 %endif
97 %endif
98 %for tag in cs.tags:
98 %for tag in cs.tags:
99 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
99 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
100 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
100 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
101 %endfor
101 %endfor
102 </span>
102 </span>
103 </div>
103 </div>
104 </div>
104 </div>
105
105
106 %endfor
106 %endfor
107 <div class="pagination-wh pagination-left">
107 <div class="pagination-wh pagination-left">
108 ${c.pagination.pager('$link_previous ~2~ $link_next')}
108 ${c.pagination.pager('$link_previous ~2~ $link_next')}
109 </div>
109 </div>
110 </div>
110 </div>
111 </div>
111 </div>
112
112
113 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
113 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
114 <script type="text/javascript">
114 <script type="text/javascript">
115 YAHOO.util.Event.onDOMReady(function(){
115 YAHOO.util.Event.onDOMReady(function(){
116
116
117 //Monitor range checkboxes and build a link to changesets
117 //Monitor range checkboxes and build a link to changesets
118 //ranges
118 //ranges
119 var checkboxes = YUD.getElementsByClassName('changeset_range');
119 var checkboxes = YUD.getElementsByClassName('changeset_range');
120 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
120 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
121 YUE.on(checkboxes,'click',function(e){
121 YUE.on(checkboxes,'click',function(e){
122 var checked_checkboxes = [];
122 var checked_checkboxes = [];
123 for (pos in checkboxes){
123 for (pos in checkboxes){
124 if(checkboxes[pos].checked){
124 if(checkboxes[pos].checked){
125 checked_checkboxes.push(checkboxes[pos]);
125 checked_checkboxes.push(checkboxes[pos]);
126 }
126 }
127 }
127 }
128 if(checked_checkboxes.length>1){
128 if(checked_checkboxes.length>1){
129 var rev_end = checked_checkboxes[0].name;
129 var rev_end = checked_checkboxes[0].name;
130 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
130 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
131
131
132 var url = url_tmpl.replace('__REVRANGE__',
132 var url = url_tmpl.replace('__REVRANGE__',
133 rev_start+'...'+rev_end);
133 rev_start+'...'+rev_end);
134
134
135 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
135 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
136 link = link.replace('__S',rev_start);
136 link = link.replace('__S',rev_start);
137 link = link.replace('__E',rev_end);
137 link = link.replace('__E',rev_end);
138 YUD.get('rev_range_container').innerHTML = link;
138 YUD.get('rev_range_container').innerHTML = link;
139 YUD.setStyle('rev_range_container','display','');
139 YUD.setStyle('rev_range_container','display','');
140 }
140 }
141 else{
141 else{
142 YUD.setStyle('rev_range_container','display','none');
142 YUD.setStyle('rev_range_container','display','none');
143
143
144 }
144 }
145 });
145 });
146
146
147 var msgs = YUQ('.message');
147 var msgs = YUQ('.message');
148 // get first element height
148 // get first element height
149 var el = YUQ('#graph_content .container')[0];
149 var el = YUQ('#graph_content .container')[0];
150 var row_h = el.clientHeight;
150 var row_h = el.clientHeight;
151 for(var i=0;i<msgs.length;i++){
151 for(var i=0;i<msgs.length;i++){
152 var m = msgs[i];
152 var m = msgs[i];
153
153
154 var h = m.clientHeight;
154 var h = m.clientHeight;
155 var pad = YUD.getStyle(m,'padding');
155 var pad = YUD.getStyle(m,'padding');
156 if(h > row_h){
156 if(h > row_h){
157 var offset = row_h - (h+12);
157 var offset = row_h - (h+12);
158 YUD.setStyle(m.nextElementSibling,'display','block');
158 YUD.setStyle(m.nextElementSibling,'display','block');
159 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
159 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
160 };
160 };
161 }
161 }
162 YUE.on(YUQ('.expand'),'click',function(e){
162 YUE.on(YUQ('.expand'),'click',function(e){
163 var elem = e.currentTarget.parentNode.parentNode;
163 var elem = e.currentTarget.parentNode.parentNode;
164 YUD.setStyle(e.currentTarget,'display','none');
164 YUD.setStyle(e.currentTarget,'display','none');
165 YUD.setStyle(elem,'height','auto');
165 YUD.setStyle(elem,'height','auto');
166
166
167 //redraw the graph, max_w and jsdata are global vars
167 //redraw the graph, max_w and jsdata are global vars
168 set_canvas(max_w);
168 set_canvas(max_w);
169
169
170 var r = new BranchRenderer();
170 var r = new BranchRenderer();
171 r.render(jsdata,max_w);
171 r.render(jsdata,max_w);
172
172
173 })
173 })
174
174
175 // Fetch changeset details
175 // Fetch changeset details
176 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
176 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
177 var id = e.currentTarget.id
177 var id = e.currentTarget.id
178 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
178 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
179 var url = url.replace('__CS__',id);
179 var url = url.replace('__CS__',id);
180 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
180 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
181 });
181 });
182
182
183 // change branch filter
183 // change branch filter
184 YUE.on(YUD.get('branch_filter'),'change',function(e){
184 YUE.on(YUD.get('branch_filter'),'change',function(e){
185 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
185 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
186 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
186 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
187 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
187 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
188 var url = url.replace('__BRANCH__',selected_branch);
188 var url = url.replace('__BRANCH__',selected_branch);
189 if(selected_branch != ''){
189 if(selected_branch != ''){
190 window.location = url;
190 window.location = url;
191 }else{
191 }else{
192 window.location = url_main;
192 window.location = url_main;
193 }
193 }
194
194
195 });
195 });
196
196
197 function set_canvas(heads) {
197 function set_canvas(heads) {
198 var c = document.getElementById('graph_nodes');
198 var c = document.getElementById('graph_nodes');
199 var t = document.getElementById('graph_content');
199 var t = document.getElementById('graph_content');
200 canvas = document.getElementById('graph_canvas');
200 canvas = document.getElementById('graph_canvas');
201 var div_h = t.clientHeight;
201 var div_h = t.clientHeight;
202 c.style.height=div_h+'px';
202 c.style.height=div_h+'px';
203 canvas.setAttribute('height',div_h);
203 canvas.setAttribute('height',div_h);
204 c.style.height=max_w+'px';
204 c.style.height=max_w+'px';
205 canvas.setAttribute('width',max_w);
205 canvas.setAttribute('width',max_w);
206 };
206 };
207 var heads = 1;
207 var heads = 1;
208 var max_heads = 0;
208 var max_heads = 0;
209 var jsdata = ${c.jsdata|n};
209 var jsdata = ${c.jsdata|n};
210
210
211 for( var i=0;i<jsdata.length;i++){
211 for( var i=0;i<jsdata.length;i++){
212 var m = Math.max.apply(Math, jsdata[i][1]);
212 var m = Math.max.apply(Math, jsdata[i][1]);
213 if (m>max_heads){
213 if (m>max_heads){
214 max_heads = m;
214 max_heads = m;
215 }
215 }
216 }
216 }
217 var max_w = Math.max(100,max_heads*25);
217 var max_w = Math.max(100,max_heads*25);
218 set_canvas(max_w);
218 set_canvas(max_w);
219
219
220 var r = new BranchRenderer();
220 var r = new BranchRenderer();
221 r.render(jsdata,max_w);
221 r.render(jsdata,max_w);
222
222
223 });
223 });
224 </script>
224 </script>
225 %else:
225 %else:
226 ${_('There are no changes yet')}
226 ${_('There are no changes yet')}
227 %endif
227 %endif
228 </div>
228 </div>
229 </div>
229 </div>
230 </%def>
230 </%def>
@@ -1,83 +1,81
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 %if c.repo_changesets:
2 %if c.repo_changesets:
3 <table class="table_disp">
3 <table class="table_disp">
4 <tr>
4 <tr>
5 <th class="left">${_('revision')}</th>
5 <th class="left">${_('revision')}</th>
6 <th class="left">${_('commit message')}</th>
6 <th class="left">${_('commit message')}</th>
7 <th class="left">${_('age')}</th>
7 <th class="left">${_('age')}</th>
8 <th class="left">${_('author')}</th>
8 <th class="left">${_('author')}</th>
9 <th class="left">${_('branch')}</th>
9 <th class="left">${_('branch')}</th>
10 <th class="left">${_('tags')}</th>
10 <th class="left">${_('tags')}</th>
11 </tr>
11 </tr>
12 %for cnt,cs in enumerate(c.repo_changesets):
12 %for cnt,cs in enumerate(c.repo_changesets):
13 <tr class="parity${cnt%2}">
13 <tr class="parity${cnt%2}">
14 <td>
14 <td>
15 <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
15 <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
16 </td>
16 </td>
17 <td>
17 <td>
18 ${h.link_to(h.truncate(cs.message,50),
18 ${h.link_to(h.truncate(cs.message,50),
19 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
19 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
20 title=cs.message)}
20 title=cs.message)}
21 </td>
21 </td>
22 <td><span class="tooltip" title="${cs.date}">
22 <td><span class="tooltip" title="${cs.date}">
23 ${h.age(cs.date)}</span>
23 ${h.age(cs.date)}</span>
24 </td>
24 </td>
25 <td title="${cs.author}">${h.person(cs.author)}</td>
25 <td title="${cs.author}">${h.person(cs.author)}</td>
26 <td>
26 <td>
27 <span class="logtags">
27 <span class="logtags">
28 <span class="branchtag">
28 <span class="branchtag">
29 %if h.is_hg(c.rhodecode_repo):
30 ${cs.branch}
29 ${cs.branch}
31 %endif
32 </span>
30 </span>
33 </span>
31 </span>
34 </td>
32 </td>
35 <td>
33 <td>
36 <span class="logtags">
34 <span class="logtags">
37 %for tag in cs.tags:
35 %for tag in cs.tags:
38 <span class="tagtag">${tag}</span>
36 <span class="tagtag">${tag}</span>
39 %endfor
37 %endfor
40 </span>
38 </span>
41 </td>
39 </td>
42 </tr>
40 </tr>
43 %endfor
41 %endfor
44
42
45 </table>
43 </table>
46
44
47 <script type="text/javascript">
45 <script type="text/javascript">
48 YUE.onDOMReady(function(){
46 YUE.onDOMReady(function(){
49 YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
47 YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
50 ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
48 ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
51 YUE.preventDefault(e);
49 YUE.preventDefault(e);
52 },'.pager_link');
50 },'.pager_link');
53 });
51 });
54 </script>
52 </script>
55
53
56 <div class="pagination-wh pagination-left">
54 <div class="pagination-wh pagination-left">
57 ${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
55 ${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
58 </div>
56 </div>
59 %else:
57 %else:
60
58
61 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
59 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
62 <h4>${_('Add or upload files directly via RhodeCode')}</h4>
60 <h4>${_('Add or upload files directly via RhodeCode')}</h4>
63 <div style="margin: 20px 30px;">
61 <div style="margin: 20px 30px;">
64 <div id="add_node_id" class="add_node">
62 <div id="add_node_id" class="add_node">
65 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
63 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
66 </div>
64 </div>
67 </div>
65 </div>
68 %endif
66 %endif
69
67
70
68
71 <h4>${_('Push new repo')}</h4>
69 <h4>${_('Push new repo')}</h4>
72 <pre>
70 <pre>
73 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
71 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
74 ${c.rhodecode_repo.alias} add README # add first file
72 ${c.rhodecode_repo.alias} add README # add first file
75 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
73 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
76 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
74 ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
77 </pre>
75 </pre>
78
76
79 <h4>${_('Existing repository?')}</h4>
77 <h4>${_('Existing repository?')}</h4>
80 <pre>
78 <pre>
81 ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
79 ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
82 </pre>
80 </pre>
83 %endif
81 %endif
General Comments 0
You need to be logged in to leave comments. Login now