##// END OF EJS Templates
controllers: consistently use c.cs_comments and cs_statuses...
Mads Kiilerich -
r6766:f0ec7be7 default
parent child Browse files
Show More
@@ -1,194 +1,194 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changelog
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changelog controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from tg import request, session, tmpl_context as c
32 32 from tg.i18n import ugettext as _
33 33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
34 34
35 35 import kallithea.lib.helpers as h
36 36 from kallithea.config.routing import url
37 37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
38 38 from kallithea.lib.base import BaseRepoController, render
39 39 from kallithea.lib.graphmod import graph_data
40 40 from kallithea.lib.page import RepoPage
41 41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
42 42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
43 43 from kallithea.lib.utils2 import safe_int, safe_str
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def _load_changelog_summary():
50 50 # also used from summary ...
51 51 p = safe_int(request.GET.get('page'), 1)
52 52 size = safe_int(request.GET.get('size'), 10)
53 53
54 54 def url_generator(**kw):
55 55 return url('changelog_summary_home',
56 56 repo_name=c.db_repo.repo_name, size=size, **kw)
57 57
58 58 collection = c.db_repo_scm_instance
59 59
60 60 c.repo_changesets = RepoPage(collection, page=p,
61 61 items_per_page=size,
62 62 url=url_generator)
63 63 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 c.comments = c.db_repo.get_comments(page_revisions)
65 c.statuses = c.db_repo.statuses(page_revisions)
64 c.cs_comments = c.db_repo.get_comments(page_revisions)
65 c.cs_statuses = c.db_repo.statuses(page_revisions)
66 66
67 67
68 68 class ChangelogController(BaseRepoController):
69 69
70 70 def _before(self, *args, **kwargs):
71 71 super(ChangelogController, self)._before(*args, **kwargs)
72 72 c.affected_files_cut_off = 60
73 73
74 74 @staticmethod
75 75 def __get_cs(rev, repo):
76 76 """
77 77 Safe way to get changeset. If error occur fail with error message.
78 78
79 79 :param rev: revision to fetch
80 80 :param repo: repo instance
81 81 """
82 82
83 83 try:
84 84 return c.db_repo_scm_instance.get_changeset(rev)
85 85 except EmptyRepositoryError as e:
86 86 h.flash(h.literal(_('There are no changesets yet')),
87 87 category='error')
88 88 except RepositoryError as e:
89 89 log.error(traceback.format_exc())
90 90 h.flash(safe_str(e), category='error')
91 91 raise HTTPBadRequest()
92 92
93 93 @LoginRequired()
94 94 @HasRepoPermissionLevelDecorator('read')
95 95 def index(self, repo_name, revision=None, f_path=None):
96 96 # Fix URL after page size form submission via GET
97 97 # TODO: Somehow just don't send this extra junk in the GET URL
98 98 if request.GET.get('set'):
99 99 request.GET.pop('set', None)
100 100 if revision is None:
101 101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
102 102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
103 103
104 104 limit = 2000
105 105 default = 100
106 106 if request.GET.get('size'):
107 107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
108 108 session['changelog_size'] = c.size
109 109 session.save()
110 110 else:
111 111 c.size = int(session.get('changelog_size', default))
112 112 # min size must be 1
113 113 c.size = max(c.size, 1)
114 114 p = safe_int(request.GET.get('page'), 1)
115 115 branch_name = request.GET.get('branch', None)
116 116 if (branch_name and
117 117 branch_name not in c.db_repo_scm_instance.branches and
118 118 branch_name not in c.db_repo_scm_instance.closed_branches and
119 119 not revision):
120 120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
121 121 revision=branch_name, f_path=f_path or ''))
122 122
123 123 if revision == 'tip':
124 124 revision = None
125 125
126 126 c.changelog_for_path = f_path
127 127 try:
128 128
129 129 if f_path:
130 130 log.debug('generating changelog for path %s', f_path)
131 131 # get the history for the file !
132 132 tip_cs = c.db_repo_scm_instance.get_changeset()
133 133 try:
134 134 collection = tip_cs.get_file_history(f_path)
135 135 except (NodeDoesNotExistError, ChangesetError):
136 136 #this node is not present at tip !
137 137 try:
138 138 cs = self.__get_cs(revision, repo_name)
139 139 collection = cs.get_file_history(f_path)
140 140 except RepositoryError as e:
141 141 h.flash(safe_str(e), category='warning')
142 142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
143 143 collection = list(reversed(collection))
144 144 else:
145 145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
146 146 branch_name=branch_name)
147 147 c.total_cs = len(collection)
148 148
149 149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
150 150 items_per_page=c.size, branch=branch_name,)
151 151
152 152 page_revisions = [x.raw_id for x in c.pagination]
153 c.comments = c.db_repo.get_comments(page_revisions)
154 c.statuses = c.db_repo.statuses(page_revisions)
153 c.cs_comments = c.db_repo.get_comments(page_revisions)
154 c.cs_statuses = c.db_repo.statuses(page_revisions)
155 155 except EmptyRepositoryError as e:
156 156 h.flash(safe_str(e), category='warning')
157 157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
158 158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
159 159 log.error(traceback.format_exc())
160 160 h.flash(safe_str(e), category='error')
161 161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
162 162
163 163 c.branch_name = branch_name
164 164 c.branch_filters = [('', _('None'))] + \
165 165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
166 166 if c.db_repo_scm_instance.closed_branches:
167 167 prefix = _('(closed)') + ' '
168 168 c.branch_filters += [('-', '-')] + \
169 169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
170 170 revs = []
171 171 if not f_path:
172 172 revs = [x.revision for x in c.pagination]
173 173 c.jsdata = graph_data(c.db_repo_scm_instance, revs)
174 174
175 175 c.revision = revision # requested revision ref
176 176 c.first_revision = c.pagination[0] # pagination is never empty here!
177 177 return render('changelog/changelog.html')
178 178
179 179 @LoginRequired()
180 180 @HasRepoPermissionLevelDecorator('read')
181 181 def changelog_details(self, cs):
182 182 if request.environ.get('HTTP_X_PARTIAL_XHR'):
183 183 c.cs = c.db_repo_scm_instance.get_changeset(cs)
184 184 return render('changelog/changelog_details.html')
185 185 raise HTTPNotFound()
186 186
187 187 @LoginRequired()
188 188 @HasRepoPermissionLevelDecorator('read')
189 189 def changelog_summary(self, repo_name):
190 190 if request.environ.get('HTTP_X_PARTIAL_XHR'):
191 191 _load_changelog_summary()
192 192
193 193 return render('changelog/changelog_summary_data.html')
194 194 raise HTTPNotFound()
@@ -1,295 +1,295 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.compare
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 compare controller showing differences between two
19 19 repos, branches, bookmarks or tips
20 20
21 21 This file was forked by the Kallithea project in July 2014.
22 22 Original author and date, and relevant copyright and licensing information is below:
23 23 :created_on: May 6, 2012
24 24 :author: marcink
25 25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 26 :license: GPLv3, see LICENSE.md for more details.
27 27 """
28 28
29 29
30 30 import logging
31 31 import re
32 32
33 33 from tg import request, tmpl_context as c
34 34 from tg.i18n import ugettext as _
35 35 from webob.exc import HTTPFound, HTTPBadRequest, HTTPNotFound
36 36
37 37 from kallithea.config.routing import url
38 38 from kallithea.lib.utils2 import safe_str, safe_int
39 39 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 40 from kallithea.lib import helpers as h
41 41 from kallithea.lib.base import BaseRepoController, render
42 42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
43 43 from kallithea.lib import diffs
44 44 from kallithea.model.db import Repository
45 45 from kallithea.lib.diffs import LimitedDiffContainer
46 46 from kallithea.controllers.changeset import _ignorews_url, _context_url
47 47 from kallithea.lib.graphmod import graph_data
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class CompareController(BaseRepoController):
53 53
54 54 def _before(self, *args, **kwargs):
55 55 super(CompareController, self)._before(*args, **kwargs)
56 56
57 57 # The base repository has already been retrieved.
58 58 c.a_repo = c.db_repo
59 59
60 60 # Retrieve the "changeset" repository (default: same as base).
61 61 other_repo = request.GET.get('other_repo', None)
62 62 if other_repo is None:
63 63 c.cs_repo = c.a_repo
64 64 else:
65 65 c.cs_repo = Repository.get_by_repo_name(other_repo)
66 66 if c.cs_repo is None:
67 67 msg = _('Could not find other repository %s') % other_repo
68 68 h.flash(msg, category='error')
69 69 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
70 70
71 71 # Verify that it's even possible to compare these two repositories.
72 72 if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias:
73 73 msg = _('Cannot compare repositories of different types')
74 74 h.flash(msg, category='error')
75 75 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
76 76
77 77 @staticmethod
78 78 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
79 79 """
80 80 Returns lists of changesets that can be merged from org_repo@org_rev
81 81 to other_repo@other_rev
82 82 ... and the other way
83 83 ... and the ancestors that would be used for merge
84 84
85 85 :param org_repo: repo object, that is most likely the original repo we forked from
86 86 :param org_rev: the revision we want our compare to be made
87 87 :param other_repo: repo object, most likely the fork of org_repo. It has
88 88 all changesets that we need to obtain
89 89 :param other_rev: revision we want out compare to be made on other_repo
90 90 """
91 91 ancestors = None
92 92 if org_rev == other_rev:
93 93 org_changesets = []
94 94 other_changesets = []
95 95
96 96 elif alias == 'hg':
97 97 #case two independent repos
98 98 if org_repo != other_repo:
99 99 hgrepo = unionrepo.unionrepository(other_repo.baseui,
100 100 other_repo.path,
101 101 org_repo.path)
102 102 # all ancestors of other_rev will be in other_repo and
103 103 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
104 104
105 105 #no remote compare do it on the same repository
106 106 else:
107 107 hgrepo = other_repo._repo
108 108
109 109 ancestors = [hgrepo[ancestor].hex() for ancestor in
110 110 hgrepo.revs("id(%s) & ::id(%s)", other_rev, org_rev)]
111 111 if ancestors:
112 112 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
113 113 else:
114 114 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
115 115 ancestors = [hgrepo[ancestor].hex() for ancestor in
116 116 hgrepo.revs("heads(::id(%s) & ::id(%s))", org_rev, other_rev)] # FIXME: expensive!
117 117
118 118 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
119 119 other_rev, org_rev, org_rev)
120 120 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
121 121 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
122 122 org_rev, other_rev, other_rev)
123 123 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
124 124
125 125 elif alias == 'git':
126 126 if org_repo != other_repo:
127 127 from dulwich.repo import Repo
128 128 from dulwich.client import SubprocessGitClient
129 129
130 130 gitrepo = Repo(org_repo.path)
131 131 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
132 132
133 133 gitrepo_remote = Repo(other_repo.path)
134 134 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
135 135
136 136 revs = [
137 137 x.commit.id
138 138 for x in gitrepo_remote.get_walker(include=[other_rev],
139 139 exclude=[org_rev])
140 140 ]
141 141 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
142 142 if other_changesets:
143 143 ancestors = [other_changesets[0].parents[0].raw_id]
144 144 else:
145 145 # no changesets from other repo, ancestor is the other_rev
146 146 ancestors = [other_rev]
147 147
148 148 gitrepo.close()
149 149 gitrepo_remote.close()
150 150
151 151 else:
152 152 so, se = org_repo.run_git_command(
153 153 ['log', '--reverse', '--pretty=format:%H',
154 154 '-s', '%s..%s' % (org_rev, other_rev)]
155 155 )
156 156 other_changesets = [org_repo.get_changeset(cs)
157 157 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
158 158 so, se = org_repo.run_git_command(
159 159 ['merge-base', org_rev, other_rev]
160 160 )
161 161 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
162 162 org_changesets = []
163 163
164 164 else:
165 165 raise Exception('Bad alias only git and hg is allowed')
166 166
167 167 return other_changesets, org_changesets, ancestors
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionLevelDecorator('read')
171 171 def index(self, repo_name):
172 172 c.compare_home = True
173 173 c.a_ref_name = c.cs_ref_name = None
174 174 return render('compare/compare_diff.html')
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionLevelDecorator('read')
178 178 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
179 179 org_ref_name = org_ref_name.strip()
180 180 other_ref_name = other_ref_name.strip()
181 181
182 182 # If merge is True:
183 183 # Show what org would get if merged with other:
184 184 # List changesets that are ancestors of other but not of org.
185 185 # New changesets in org is thus ignored.
186 186 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
187 187 # If merge is False:
188 188 # Make a raw diff from org to other, no matter if related or not.
189 189 # Changesets in one and not in the other will be ignored
190 190 merge = bool(request.GET.get('merge'))
191 191 # fulldiff disables cut_off_limit
192 192 c.fulldiff = request.GET.get('fulldiff')
193 193 # partial uses compare_cs.html template directly
194 194 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
195 195 # is_ajax_preview puts hidden input field with changeset revisions
196 196 c.is_ajax_preview = partial and request.GET.get('is_ajax_preview')
197 197 # swap url for compare_diff page - never partial and never is_ajax_preview
198 198 c.swap_url = h.url('compare_url',
199 199 repo_name=c.cs_repo.repo_name,
200 200 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
201 201 other_repo=c.a_repo.repo_name,
202 202 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
203 203 merge=merge or '')
204 204
205 205 # set callbacks for generating markup for icons
206 206 c.ignorews_url = _ignorews_url
207 207 c.context_url = _context_url
208 208 ignore_whitespace = request.GET.get('ignorews') == '1'
209 209 line_context = safe_int(request.GET.get('context'), 3)
210 210
211 211 c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name,
212 212 returnempty=True)
213 213 c.cs_rev = self._get_ref_rev(c.cs_repo, other_ref_type, other_ref_name)
214 214
215 215 c.compare_home = False
216 216 c.a_ref_name = org_ref_name
217 217 c.a_ref_type = org_ref_type
218 218 c.cs_ref_name = other_ref_name
219 219 c.cs_ref_type = other_ref_type
220 220
221 221 c.cs_ranges, c.cs_ranges_org, c.ancestors = self._get_changesets(
222 222 c.a_repo.scm_instance.alias, c.a_repo.scm_instance, c.a_rev,
223 223 c.cs_repo.scm_instance, c.cs_rev)
224 224 raw_ids = [x.raw_id for x in c.cs_ranges]
225 225 c.cs_comments = c.cs_repo.get_comments(raw_ids)
226 c.statuses = c.cs_repo.statuses(raw_ids)
226 c.cs_statuses = c.cs_repo.statuses(raw_ids)
227 227
228 228 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
229 229 c.jsdata = graph_data(c.cs_repo.scm_instance, revs)
230 230
231 231 if partial:
232 232 return render('compare/compare_cs.html')
233 233
234 234 org_repo = c.a_repo
235 235 other_repo = c.cs_repo
236 236
237 237 if merge:
238 238 rev1 = msg = None
239 239 if not c.cs_ranges:
240 240 msg = _('Cannot show empty diff')
241 241 elif not c.ancestors:
242 242 msg = _('No ancestor found for merge diff')
243 243 elif len(c.ancestors) == 1:
244 244 rev1 = c.ancestors[0]
245 245 else:
246 246 msg = _('Multiple merge ancestors found for merge compare')
247 247 if rev1 is None:
248 248 h.flash(msg, category='error')
249 249 log.error(msg)
250 250 raise HTTPNotFound
251 251
252 252 # case we want a simple diff without incoming changesets,
253 253 # previewing what will be merged.
254 254 # Make the diff on the other repo (which is known to have other_rev)
255 255 log.debug('Using ancestor %s as rev1 instead of %s',
256 256 rev1, c.a_rev)
257 257 org_repo = other_repo
258 258 else: # comparing tips, not necessarily linearly related
259 259 if org_repo != other_repo:
260 260 # TODO: we could do this by using hg unionrepo
261 261 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
262 262 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
263 263 raise HTTPBadRequest
264 264 rev1 = c.a_rev
265 265
266 266 diff_limit = self.cut_off_limit if not c.fulldiff else None
267 267
268 268 log.debug('running diff between %s and %s in %s',
269 269 rev1, c.cs_rev, org_repo.scm_instance.path)
270 270 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
271 271 ignore_whitespace=ignore_whitespace,
272 272 context=line_context)
273 273
274 274 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
275 275 diff_limit=diff_limit)
276 276 _parsed = diff_processor.prepare()
277 277
278 278 c.limited_diff = False
279 279 if isinstance(_parsed, LimitedDiffContainer):
280 280 c.limited_diff = True
281 281
282 282 c.file_diff_data = []
283 283 c.lines_added = 0
284 284 c.lines_deleted = 0
285 285 for f in _parsed:
286 286 st = f['stats']
287 287 c.lines_added += st['added']
288 288 c.lines_deleted += st['deleted']
289 289 filename = f['filename']
290 290 fid = h.FID('', filename)
291 291 diff = diff_processor.as_html(enable_comments=False,
292 292 parsed_lines=[f])
293 293 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
294 294
295 295 return render('compare/compare_diff.html')
@@ -1,747 +1,746 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.pullrequests
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pull requests controller for Kallithea for initializing pull requests
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 7, 2012
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from tg import request, tmpl_context as c
33 33 from tg.i18n import ugettext as _
34 34 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
35 35
36 36 from kallithea.config.routing import url
37 37 from kallithea.lib import helpers as h
38 38 from kallithea.lib import diffs
39 39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
40 40 NotAnonymous
41 41 from kallithea.lib.base import BaseRepoController, render, jsonify
42 42 from kallithea.lib.diffs import LimitedDiffContainer
43 43 from kallithea.lib.page import Page
44 44 from kallithea.lib.utils import action_logger
45 45 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
46 46 from kallithea.lib.vcs.utils import safe_str
47 47 from kallithea.lib.vcs.utils.hgcompat import unionrepo
48 48 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
49 49 PullRequestReviewer, Repository, User
50 50 from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
51 51 from kallithea.model.meta import Session
52 52 from kallithea.model.repo import RepoModel
53 53 from kallithea.model.comment import ChangesetCommentsModel
54 54 from kallithea.model.changeset_status import ChangesetStatusModel
55 55 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
56 56 from kallithea.lib.utils2 import safe_int
57 57 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
58 58 create_comment
59 59 from kallithea.controllers.compare import CompareController
60 60 from kallithea.lib.graphmod import graph_data
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def _get_reviewer(user_id):
66 66 """Look up user by ID and validate it as a potential reviewer."""
67 67 try:
68 68 user = User.get(int(user_id))
69 69 except ValueError:
70 70 user = None
71 71
72 72 if user is None or user.is_default_user:
73 73 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
74 74 raise HTTPBadRequest()
75 75
76 76 return user
77 77
78 78
79 79 class PullrequestsController(BaseRepoController):
80 80
81 81 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
82 82 """return a structure with repo's interesting changesets, suitable for
83 83 the selectors in pullrequest.html
84 84
85 85 rev: a revision that must be in the list somehow and selected by default
86 86 branch: a branch that must be in the list and selected by default - even if closed
87 87 branch_rev: a revision of which peers should be preferred and available."""
88 88 # list named branches that has been merged to this named branch - it should probably merge back
89 89 peers = []
90 90
91 91 if rev:
92 92 rev = safe_str(rev)
93 93
94 94 if branch:
95 95 branch = safe_str(branch)
96 96
97 97 if branch_rev:
98 98 branch_rev = safe_str(branch_rev)
99 99 # a revset not restricting to merge() would be better
100 100 # (especially because it would get the branch point)
101 101 # ... but is currently too expensive
102 102 # including branches of children could be nice too
103 103 peerbranches = set()
104 104 for i in repo._repo.revs(
105 105 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
106 106 branch_rev, branch_rev):
107 107 abranch = repo.get_changeset(i).branch
108 108 if abranch not in peerbranches:
109 109 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
110 110 peers.append((n, abranch))
111 111 peerbranches.add(abranch)
112 112
113 113 selected = None
114 114 tiprev = repo.tags.get('tip')
115 115 tipbranch = None
116 116
117 117 branches = []
118 118 for abranch, branchrev in repo.branches.iteritems():
119 119 n = 'branch:%s:%s' % (abranch, branchrev)
120 120 desc = abranch
121 121 if branchrev == tiprev:
122 122 tipbranch = abranch
123 123 desc = '%s (current tip)' % desc
124 124 branches.append((n, desc))
125 125 if rev == branchrev:
126 126 selected = n
127 127 if branch == abranch:
128 128 if not rev:
129 129 selected = n
130 130 branch = None
131 131 if branch: # branch not in list - it is probably closed
132 132 branchrev = repo.closed_branches.get(branch)
133 133 if branchrev:
134 134 n = 'branch:%s:%s' % (branch, branchrev)
135 135 branches.append((n, _('%s (closed)') % branch))
136 136 selected = n
137 137 branch = None
138 138 if branch:
139 139 log.debug('branch %r not found in %s', branch, repo)
140 140
141 141 bookmarks = []
142 142 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
143 143 n = 'book:%s:%s' % (bookmark, bookmarkrev)
144 144 bookmarks.append((n, bookmark))
145 145 if rev == bookmarkrev:
146 146 selected = n
147 147
148 148 tags = []
149 149 for tag, tagrev in repo.tags.iteritems():
150 150 if tag == 'tip':
151 151 continue
152 152 n = 'tag:%s:%s' % (tag, tagrev)
153 153 tags.append((n, tag))
154 154 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
155 155
156 156 # prio 1: rev was selected as existing entry above
157 157
158 158 # prio 2: create special entry for rev; rev _must_ be used
159 159 specials = []
160 160 if rev and selected is None:
161 161 selected = 'rev:%s:%s' % (rev, rev)
162 162 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
163 163
164 164 # prio 3: most recent peer branch
165 165 if peers and not selected:
166 166 selected = peers[0][0]
167 167
168 168 # prio 4: tip revision
169 169 if not selected:
170 170 if h.is_hg(repo):
171 171 if tipbranch:
172 172 selected = 'branch:%s:%s' % (tipbranch, tiprev)
173 173 else:
174 174 selected = 'tag:null:' + repo.EMPTY_CHANGESET
175 175 tags.append((selected, 'null'))
176 176 else:
177 177 if 'master' in repo.branches:
178 178 selected = 'branch:master:%s' % repo.branches['master']
179 179 else:
180 180 k, v = repo.branches.items()[0]
181 181 selected = 'branch:%s:%s' % (k, v)
182 182
183 183 groups = [(specials, _("Special")),
184 184 (peers, _("Peer branches")),
185 185 (bookmarks, _("Bookmarks")),
186 186 (branches, _("Branches")),
187 187 (tags, _("Tags")),
188 188 ]
189 189 return [g for g in groups if g[0]], selected
190 190
191 191 def _get_is_allowed_change_status(self, pull_request):
192 192 if pull_request.is_closed():
193 193 return False
194 194
195 195 owner = request.authuser.user_id == pull_request.owner_id
196 196 reviewer = PullRequestReviewer.query() \
197 197 .filter(PullRequestReviewer.pull_request == pull_request) \
198 198 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
199 199 .count() != 0
200 200
201 201 return request.authuser.admin or owner or reviewer
202 202
203 203 @LoginRequired()
204 204 @HasRepoPermissionLevelDecorator('read')
205 205 def show_all(self, repo_name):
206 206 c.from_ = request.GET.get('from_') or ''
207 207 c.closed = request.GET.get('closed') or ''
208 208 p = safe_int(request.GET.get('page'), 1)
209 209
210 210 q = PullRequest.query(include_closed=c.closed, sorted=True)
211 211 if c.from_:
212 212 q = q.filter_by(org_repo=c.db_repo)
213 213 else:
214 214 q = q.filter_by(other_repo=c.db_repo)
215 215 c.pull_requests = q.all()
216 216
217 217 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
218 218
219 219 return render('/pullrequests/pullrequest_show_all.html')
220 220
221 221 @LoginRequired()
222 222 @NotAnonymous()
223 223 def show_my(self):
224 224 c.closed = request.GET.get('closed') or ''
225 225
226 226 c.my_pull_requests = PullRequest.query(
227 227 include_closed=c.closed,
228 228 sorted=True,
229 229 ).filter_by(owner_id=request.authuser.user_id).all()
230 230
231 231 c.participate_in_pull_requests = []
232 232 c.participate_in_pull_requests_todo = []
233 233 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
234 234 for pr in PullRequest.query(
235 235 include_closed=c.closed,
236 236 reviewer_id=request.authuser.user_id,
237 237 sorted=True,
238 238 ):
239 239 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
240 240 if status in done_status:
241 241 c.participate_in_pull_requests.append(pr)
242 242 else:
243 243 c.participate_in_pull_requests_todo.append(pr)
244 244
245 245 return render('/pullrequests/pullrequest_show_my.html')
246 246
247 247 @LoginRequired()
248 248 @NotAnonymous()
249 249 @HasRepoPermissionLevelDecorator('read')
250 250 def index(self):
251 251 org_repo = c.db_repo
252 252 org_scm_instance = org_repo.scm_instance
253 253 try:
254 254 org_scm_instance.get_changeset()
255 255 except EmptyRepositoryError as e:
256 256 h.flash(h.literal(_('There are no changesets yet')),
257 257 category='warning')
258 258 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
259 259
260 260 org_rev = request.GET.get('rev_end')
261 261 # rev_start is not directly useful - its parent could however be used
262 262 # as default for other and thus give a simple compare view
263 263 rev_start = request.GET.get('rev_start')
264 264 other_rev = None
265 265 if rev_start:
266 266 starters = org_repo.get_changeset(rev_start).parents
267 267 if starters:
268 268 other_rev = starters[0].raw_id
269 269 else:
270 270 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
271 271 branch = request.GET.get('branch')
272 272
273 273 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
274 274 c.default_cs_repo = org_repo.repo_name
275 275 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
276 276
277 277 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
278 278 if default_cs_ref_type != 'branch':
279 279 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
280 280
281 281 # add org repo to other so we can open pull request against peer branches on itself
282 282 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
283 283
284 284 if org_repo.parent:
285 285 # add parent of this fork also and select it.
286 286 # use the same branch on destination as on source, if available.
287 287 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
288 288 c.a_repo = org_repo.parent
289 289 c.a_refs, c.default_a_ref = self._get_repo_refs(
290 290 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
291 291
292 292 else:
293 293 c.a_repo = org_repo
294 294 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
295 295
296 296 # gather forks and add to this list ... even though it is rare to
297 297 # request forks to pull from their parent
298 298 for fork in org_repo.forks:
299 299 c.a_repos.append((fork.repo_name, fork.repo_name))
300 300
301 301 return render('/pullrequests/pullrequest.html')
302 302
303 303 @LoginRequired()
304 304 @NotAnonymous()
305 305 @HasRepoPermissionLevelDecorator('read')
306 306 @jsonify
307 307 def repo_info(self, repo_name):
308 308 repo = c.db_repo
309 309 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
310 310 return {
311 311 'description': repo.description.split('\n', 1)[0],
312 312 'selected_ref': selected_ref,
313 313 'refs': refs,
314 314 }
315 315
316 316 @LoginRequired()
317 317 @NotAnonymous()
318 318 @HasRepoPermissionLevelDecorator('read')
319 319 def create(self, repo_name):
320 320 repo = c.db_repo
321 321 try:
322 322 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
323 323 except formencode.Invalid as errors:
324 324 log.error(traceback.format_exc())
325 325 log.error(str(errors))
326 326 msg = _('Error creating pull request: %s') % errors.msg
327 327 h.flash(msg, 'error')
328 328 raise HTTPBadRequest
329 329
330 330 # heads up: org and other might seem backward here ...
331 331 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
332 332 org_repo = Repository.guess_instance(_form['org_repo'])
333 333
334 334 other_ref = _form['other_ref'] # will have symbolic name and head revision
335 335 other_repo = Repository.guess_instance(_form['other_repo'])
336 336
337 337 reviewers = []
338 338
339 339 title = _form['pullrequest_title']
340 340 description = _form['pullrequest_desc'].strip()
341 341 owner = User.get(request.authuser.user_id)
342 342
343 343 try:
344 344 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
345 345 except CreatePullRequestAction.ValidationError as e:
346 346 h.flash(str(e), category='error', logf=log.error)
347 347 raise HTTPNotFound
348 348
349 349 try:
350 350 pull_request = cmd.execute()
351 351 Session().commit()
352 352 except Exception:
353 353 h.flash(_('Error occurred while creating pull request'),
354 354 category='error')
355 355 log.error(traceback.format_exc())
356 356 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
357 357
358 358 h.flash(_('Successfully opened new pull request'),
359 359 category='success')
360 360 raise HTTPFound(location=pull_request.url())
361 361
362 362 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
363 363 owner = User.get(request.authuser.user_id)
364 364 new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
365 365 new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
366 366 try:
367 367 cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
368 368 except CreatePullRequestAction.ValidationError as e:
369 369 h.flash(str(e), category='error', logf=log.error)
370 370 raise HTTPNotFound
371 371
372 372 try:
373 373 pull_request = cmd.execute()
374 374 Session().commit()
375 375 except Exception:
376 376 h.flash(_('Error occurred while creating pull request'),
377 377 category='error')
378 378 log.error(traceback.format_exc())
379 379 raise HTTPFound(location=old_pull_request.url())
380 380
381 381 h.flash(_('New pull request iteration created'),
382 382 category='success')
383 383 raise HTTPFound(location=pull_request.url())
384 384
385 385 # pullrequest_post for PR editing
386 386 @LoginRequired()
387 387 @NotAnonymous()
388 388 @HasRepoPermissionLevelDecorator('read')
389 389 def post(self, repo_name, pull_request_id):
390 390 pull_request = PullRequest.get_or_404(pull_request_id)
391 391 if pull_request.is_closed():
392 392 raise HTTPForbidden()
393 393 assert pull_request.other_repo.repo_name == repo_name
394 394 #only owner or admin can update it
395 395 owner = pull_request.owner_id == request.authuser.user_id
396 396 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
397 397 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
398 398 raise HTTPForbidden()
399 399
400 400 _form = PullRequestPostForm()().to_python(request.POST)
401 401
402 402 cur_reviewers = set(pull_request.get_reviewer_users())
403 403 new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
404 404 old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
405 405
406 406 other_added = cur_reviewers - old_reviewers
407 407 other_removed = old_reviewers - cur_reviewers
408 408
409 409 if other_added:
410 410 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
411 411 (', '.join(u.username for u in other_added)),
412 412 category='warning')
413 413 if other_removed:
414 414 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
415 415 (', '.join(u.username for u in other_removed)),
416 416 category='warning')
417 417
418 418 if _form['updaterev']:
419 419 return self.create_new_iteration(pull_request,
420 420 _form['updaterev'],
421 421 _form['pullrequest_title'],
422 422 _form['pullrequest_desc'],
423 423 new_reviewers)
424 424
425 425 added_reviewers = new_reviewers - old_reviewers - cur_reviewers
426 426 removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
427 427
428 428 old_description = pull_request.description
429 429 pull_request.title = _form['pullrequest_title']
430 430 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
431 431 pull_request.owner = User.get_by_username(_form['owner'])
432 432 user = User.get(request.authuser.user_id)
433 433
434 434 PullRequestModel().mention_from_description(user, pull_request, old_description)
435 435 PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
436 436 PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
437 437
438 438 Session().commit()
439 439 h.flash(_('Pull request updated'), category='success')
440 440
441 441 raise HTTPFound(location=pull_request.url())
442 442
443 443 @LoginRequired()
444 444 @NotAnonymous()
445 445 @HasRepoPermissionLevelDecorator('read')
446 446 @jsonify
447 447 def delete(self, repo_name, pull_request_id):
448 448 pull_request = PullRequest.get_or_404(pull_request_id)
449 449 #only owner can delete it !
450 450 if pull_request.owner_id == request.authuser.user_id:
451 451 PullRequestModel().delete(pull_request)
452 452 Session().commit()
453 453 h.flash(_('Successfully deleted pull request'),
454 454 category='success')
455 455 raise HTTPFound(location=url('my_pullrequests'))
456 456 raise HTTPForbidden()
457 457
458 458 @LoginRequired()
459 459 @HasRepoPermissionLevelDecorator('read')
460 460 def show(self, repo_name, pull_request_id, extra=None):
461 461 repo_model = RepoModel()
462 462 c.users_array = repo_model.get_users_js()
463 463 c.user_groups_array = repo_model.get_user_groups_js()
464 464 c.pull_request = PullRequest.get_or_404(pull_request_id)
465 465 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
466 466 cc_model = ChangesetCommentsModel()
467 467 cs_model = ChangesetStatusModel()
468 468
469 469 # pull_requests repo_name we opened it against
470 470 # ie. other_repo must match
471 471 if repo_name != c.pull_request.other_repo.repo_name:
472 472 raise HTTPNotFound
473 473
474 474 # load compare data into template context
475 475 c.cs_repo = c.pull_request.org_repo
476 476 (c.cs_ref_type,
477 477 c.cs_ref_name,
478 478 c.cs_rev) = c.pull_request.org_ref.split(':')
479 479
480 480 c.a_repo = c.pull_request.other_repo
481 481 (c.a_ref_type,
482 482 c.a_ref_name,
483 483 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
484 484
485 485 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
486 486 try:
487 487 c.cs_ranges = [org_scm_instance.get_changeset(x)
488 488 for x in c.pull_request.revisions]
489 489 except ChangesetDoesNotExistError:
490 490 c.cs_ranges = []
491 491 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
492 492 'error')
493 493 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
494 494 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
495 495 c.jsdata = graph_data(org_scm_instance, revs)
496 496
497 497 c.is_range = False
498 498 try:
499 499 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
500 500 cs_a = org_scm_instance.get_changeset(c.a_rev)
501 501 root_parents = c.cs_ranges[0].parents
502 502 c.is_range = cs_a in root_parents
503 503 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
504 504 except ChangesetDoesNotExistError: # probably because c.a_rev not found
505 505 pass
506 506 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
507 507 pass
508 508
509 509 avail_revs = set()
510 510 avail_show = []
511 511 c.cs_branch_name = c.cs_ref_name
512 512 c.a_branch_name = None
513 513 other_scm_instance = c.a_repo.scm_instance
514 514 c.update_msg = ""
515 515 c.update_msg_other = ""
516 516 try:
517 517 if not c.cs_ranges:
518 518 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
519 519 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
520 520 if c.cs_ref_type != 'branch':
521 521 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
522 522 c.a_branch_name = c.a_ref_name
523 523 if c.a_ref_type != 'branch':
524 524 try:
525 525 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
526 526 except EmptyRepositoryError:
527 527 c.a_branch_name = 'null' # not a branch name ... but close enough
528 528 # candidates: descendants of old head that are on the right branch
529 529 # and not are the old head itself ...
530 530 # and nothing at all if old head is a descendant of target ref name
531 531 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
532 532 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
533 533 elif c.pull_request.is_closed():
534 534 c.update_msg = _('This pull request has been closed and can not be updated.')
535 535 else: # look for descendants of PR head on source branch in org repo
536 536 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
537 537 revs[0], c.cs_branch_name)
538 538 if len(avail_revs) > 1: # more than just revs[0]
539 539 # also show changesets that not are descendants but would be merged in
540 540 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
541 541 if org_scm_instance.path != other_scm_instance.path:
542 542 # Note: org_scm_instance.path must come first so all
543 543 # valid revision numbers are 100% org_scm compatible
544 544 # - both for avail_revs and for revset results
545 545 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
546 546 org_scm_instance.path,
547 547 other_scm_instance.path)
548 548 else:
549 549 hgrepo = org_scm_instance._repo
550 550 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
551 551 avail_revs, revs[0], targethead))
552 552 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
553 553 else:
554 554 show = set()
555 555 avail_revs = set() # drop revs[0]
556 556 c.update_msg = _('No additional changesets found for iterating on this pull request.')
557 557
558 558 # TODO: handle branch heads that not are tip-most
559 559 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
560 560 if brevs:
561 561 # also show changesets that are on branch but neither ancestors nor descendants
562 562 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
563 563 show.add(revs[0]) # make sure graph shows this so we can see how they relate
564 564 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
565 565 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
566 566
567 567 avail_show = sorted(show, reverse=True)
568 568
569 569 elif org_scm_instance.alias == 'git':
570 570 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
571 571 c.update_msg = _("Git pull requests don't support iterating yet.")
572 572 except ChangesetDoesNotExistError:
573 573 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
574 574
575 575 c.avail_revs = avail_revs
576 576 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
577 577 c.avail_jsdata = graph_data(org_scm_instance, avail_show)
578 578
579 579 raw_ids = [x.raw_id for x in c.cs_ranges]
580 580 c.cs_comments = c.cs_repo.get_comments(raw_ids)
581 c.statuses = c.cs_repo.statuses(raw_ids)
581 c.cs_statuses = c.cs_repo.statuses(raw_ids)
582 582
583 583 ignore_whitespace = request.GET.get('ignorews') == '1'
584 584 line_context = safe_int(request.GET.get('context'), 3)
585 585 c.ignorews_url = _ignorews_url
586 586 c.context_url = _context_url
587 587 c.fulldiff = request.GET.get('fulldiff')
588 588 diff_limit = self.cut_off_limit if not c.fulldiff else None
589 589
590 590 # we swap org/other ref since we run a simple diff on one repo
591 591 log.debug('running diff between %s and %s in %s',
592 592 c.a_rev, c.cs_rev, org_scm_instance.path)
593 593 try:
594 594 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
595 595 ignore_whitespace=ignore_whitespace,
596 596 context=line_context)
597 597 except ChangesetDoesNotExistError:
598 598 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
599 599 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
600 600 diff_limit=diff_limit)
601 601 _parsed = diff_processor.prepare()
602 602
603 603 c.limited_diff = False
604 604 if isinstance(_parsed, LimitedDiffContainer):
605 605 c.limited_diff = True
606 606
607 607 c.file_diff_data = []
608 608 c.lines_added = 0
609 609 c.lines_deleted = 0
610 610
611 611 for f in _parsed:
612 612 st = f['stats']
613 613 c.lines_added += st['added']
614 614 c.lines_deleted += st['deleted']
615 615 filename = f['filename']
616 616 fid = h.FID('', filename)
617 617 diff = diff_processor.as_html(enable_comments=True,
618 618 parsed_lines=[f])
619 619 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
620 620
621 621 # inline comments
622 622 c.inline_cnt = 0
623 623 c.inline_comments = cc_model.get_inline_comments(
624 624 c.db_repo.repo_id,
625 625 pull_request=pull_request_id)
626 626 # count inline comments
627 627 for __, lines in c.inline_comments:
628 628 for comments in lines.values():
629 629 c.inline_cnt += len(comments)
630 630 # comments
631 c.comments = cc_model.get_comments(c.db_repo.repo_id,
632 pull_request=pull_request_id)
631 c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id)
633 632
634 633 # (badly named) pull-request status calculation based on reviewer votes
635 634 (c.pull_request_reviewers,
636 635 c.pull_request_pending_reviewers,
637 636 c.current_voting_result,
638 637 ) = cs_model.calculate_pull_request_result(c.pull_request)
639 638 c.changeset_statuses = ChangesetStatus.STATUSES
640 639
641 640 c.is_ajax_preview = False
642 641 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
643 642 return render('/pullrequests/pullrequest_show.html')
644 643
645 644 @LoginRequired()
646 645 @NotAnonymous()
647 646 @HasRepoPermissionLevelDecorator('read')
648 647 @jsonify
649 648 def comment(self, repo_name, pull_request_id):
650 649 pull_request = PullRequest.get_or_404(pull_request_id)
651 650
652 651 status = request.POST.get('changeset_status')
653 652 close_pr = request.POST.get('save_close')
654 653 delete = request.POST.get('save_delete')
655 654 f_path = request.POST.get('f_path')
656 655 line_no = request.POST.get('line')
657 656
658 657 if (status or close_pr or delete) and (f_path or line_no):
659 658 # status votes and closing is only possible in general comments
660 659 raise HTTPBadRequest()
661 660
662 661 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
663 662 if not allowed_to_change_status:
664 663 if status or close_pr:
665 664 h.flash(_('No permission to change pull request status'), 'error')
666 665 raise HTTPForbidden()
667 666
668 667 if delete == "delete":
669 668 if (pull_request.owner_id == request.authuser.user_id or
670 669 h.HasPermissionAny('hg.admin')() or
671 670 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
672 671 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
673 672 ) and not pull_request.is_closed():
674 673 PullRequestModel().delete(pull_request)
675 674 Session().commit()
676 675 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
677 676 category='success')
678 677 return {
679 678 'location': url('my_pullrequests'), # or repo pr list?
680 679 }
681 680 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
682 681 raise HTTPForbidden()
683 682
684 683 text = request.POST.get('text', '').strip()
685 684
686 685 comment = create_comment(
687 686 text,
688 687 status,
689 688 pull_request_id=pull_request_id,
690 689 f_path=f_path,
691 690 line_no=line_no,
692 691 closing_pr=close_pr,
693 692 )
694 693
695 694 action_logger(request.authuser,
696 695 'user_commented_pull_request:%s' % pull_request_id,
697 696 c.db_repo, request.ip_addr)
698 697
699 698 if status:
700 699 ChangesetStatusModel().set_status(
701 700 c.db_repo.repo_id,
702 701 status,
703 702 request.authuser.user_id,
704 703 comment,
705 704 pull_request=pull_request_id
706 705 )
707 706
708 707 if close_pr:
709 708 PullRequestModel().close_pull_request(pull_request_id)
710 709 action_logger(request.authuser,
711 710 'user_closed_pull_request:%s' % pull_request_id,
712 711 c.db_repo, request.ip_addr)
713 712
714 713 Session().commit()
715 714
716 715 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
717 716 raise HTTPFound(location=pull_request.url())
718 717
719 718 data = {
720 719 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
721 720 }
722 721 if comment is not None:
723 722 c.comment = comment
724 723 data.update(comment.get_dict())
725 724 data.update({'rendered_text':
726 725 render('changeset/changeset_comment_block.html')})
727 726
728 727 return data
729 728
730 729 @LoginRequired()
731 730 @NotAnonymous()
732 731 @HasRepoPermissionLevelDecorator('read')
733 732 @jsonify
734 733 def delete_comment(self, repo_name, comment_id):
735 734 co = ChangesetComment.get(comment_id)
736 735 if co.pull_request.is_closed():
737 736 #don't allow deleting comments on closed pull request
738 737 raise HTTPForbidden()
739 738
740 739 owner = co.author_id == request.authuser.user_id
741 740 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
742 741 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
743 742 ChangesetCommentsModel().delete(comment=co)
744 743 Session().commit()
745 744 return True
746 745 else:
747 746 raise HTTPForbidden()
@@ -1,335 +1,335 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%block name="title">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 </%block>
11 11
12 12 <%def name="breadcrumbs_links()">
13 13 <% size = c.size if c.size <= c.total_cs else c.total_cs %>
14 14 ${_('Changelog')}
15 15 %if c.changelog_for_path:
16 16 - /${c.changelog_for_path}
17 17 %endif
18 18 %if c.revision:
19 19 @ ${h.short_id(c.first_revision.raw_id)}
20 20 %endif
21 21 - ${ungettext('showing %d out of %d revision', 'showing %d out of %d revisions', size) % (size, c.total_cs)}
22 22 </%def>
23 23
24 24 <%block name="header_menu">
25 25 ${self.menu('repositories')}
26 26 </%block>
27 27
28 28 <%def name="main()">
29 29 ${self.repo_context_bar('changelog', c.first_revision.raw_id if c.first_revision else None)}
30 30 <div class="panel panel-primary">
31 31 <div class="panel-heading clearfix">
32 32 ${self.breadcrumbs()}
33 33 </div>
34 34 <div class="panel-body changelog-panel">
35 35 %if c.pagination:
36 36 <div class="changelog-heading clearfix" style="${'display:none' if c.changelog_for_path else ''}">
37 37 <div class="pull-left">
38 38 ${h.form(h.url.current(),method='get',class_="form-inline")}
39 39 ${h.submit('set',_('Show'),class_="btn btn-default btn-sm")}
40 40 ${h.text('size',size=3,value=c.size,class_='form-control')}
41 41 ${_('revisions')}
42 42 %if c.branch_name:
43 43 ${h.hidden('branch', c.branch_name)}
44 44 %endif
45 45 <a href="#" class="btn btn-default btn-sm" id="rev_range_clear" style="display:none">${_('Clear selection')}</a>
46 46 ${h.end_form()}
47 47 </div>
48 48 <div class="pull-right">
49 49 <a href="#" class="btn btn-default btn-sm" id="rev_range_container" style="display:none"></a>
50 50 %if c.revision:
51 51 <a class="btn btn-default btn-sm" href="${h.url('changelog_home', repo_name=c.repo_name)}">
52 52 ${_('Go to tip of repository')}
53 53 </a>
54 54 %endif
55 55 %if c.db_repo.fork:
56 56 <a id="compare_fork"
57 57 title="${_('Compare fork with %s' % c.db_repo.fork.repo_name)}"
58 58 href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1],other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}"
59 59 class="btn btn-default btn-sm"><i class="icon-git-compare"></i> ${_('Compare fork with parent repository (%s)' % c.db_repo.fork.repo_name)}</a>
60 60 %endif
61 61 ## text and href of open_new_pr is controlled from javascript
62 62 <a id="open_new_pr" class="btn btn-default btn-sm"></a>
63 63 ${_("Branch filter:")} ${h.select('branch_filter',c.branch_name,c.branch_filters)}
64 64 </div>
65 65 </div>
66 66
67 67 <div id="graph_nodes">
68 68 <canvas id="graph_canvas" style="width:0"></canvas>
69 69 </div>
70 70 <div id="graph_content" style="${'margin: 0px' if c.changelog_for_path else ''}">
71 71
72 72 <table class="table" id="changesets">
73 73 <tbody>
74 74 %for cnt,cs in enumerate(c.pagination):
75 75 <tr id="chg_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 else ''}">
76 76 <td class="checkbox-column">
77 77 %if c.changelog_for_path:
78 78 ${h.checkbox(cs.raw_id,class_="changeset_range", disabled="disabled")}
79 79 %else:
80 80 ${h.checkbox(cs.raw_id,class_="changeset_range")}
81 81 %endif
82 82 </td>
83 83 <td class="status">
84 %if c.statuses.get(cs.raw_id):
85 %if c.statuses.get(cs.raw_id)[2]:
86 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username, c.statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
87 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
84 %if c.cs_statuses.get(cs.raw_id):
85 %if c.cs_statuses.get(cs.raw_id)[2]:
86 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.cs_statuses.get(cs.raw_id)[1], c.cs_statuses.get(cs.raw_id)[5].username, c.cs_statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.cs_statuses.get(cs.raw_id)[3],pull_request_id=c.cs_statuses.get(cs.raw_id)[2])}">
87 <i class="icon-circle changeset-status-${c.cs_statuses.get(cs.raw_id)[0]}"></i>
88 88 </a>
89 89 %else:
90 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username)}"
91 href="${c.comments[cs.raw_id][0].url()}">
92 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
90 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.cs_statuses.get(cs.raw_id)[1], c.cs_statuses.get(cs.raw_id)[5].username)}"
91 href="${c.cs_comments[cs.raw_id][0].url()}">
92 <i class="icon-circle changeset-status-${c.cs_statuses.get(cs.raw_id)[0]}"></i>
93 93 </a>
94 94 %endif
95 95 %endif
96 96 </td>
97 97 <td class="author" data-toggle="tooltip" title="${cs.author}">
98 98 ${h.gravatar(h.email_or_none(cs.author), size=16)}
99 99 <span class="user">${h.person(cs.author)}</span>
100 100 </td>
101 101 <td class="hash">
102 102 ${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id), class_='changeset_hash')}
103 103 </td>
104 104 <td class="date">
105 105 <div class="date" data-toggle="tooltip" title="${h.fmt_date(cs.date)}">${h.age(cs.date,True)}</div>
106 106 </td>
107 107 <% message_lines = cs.message.splitlines() %>
108 108 %if len(message_lines) > 1:
109 109 <td class="expand_commit" title="${_('Expand commit message')}">
110 110 <i class="icon-align-left"></i>
111 111 </td>
112 112 %else:
113 113 <td></td>
114 114 %endif
115 115 <td class="mid">
116 116 <div class="log-container">
117 117 <div class="message">
118 118 <div class="message-firstline">${h.urlify_text(message_lines[0], c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
119 119 %if len(message_lines) > 1:
120 120 <div class="message-full hidden">${h.urlify_text(cs.message, c.repo_name)}</div>
121 121 %endif
122 122 </div>
123 123 <div class="extra-container">
124 %if c.comments.get(cs.raw_id):
125 <a class="comments-container comments-cnt" href="${c.comments[cs.raw_id][0].url()}" data-toggle="tooltip" title="${_('%s comments') % len(c.comments[cs.raw_id])}">
126 ${len(c.comments[cs.raw_id])}
124 %if c.cs_comments.get(cs.raw_id):
125 <a class="comments-container comments-cnt" href="${c.cs_comments[cs.raw_id][0].url()}" data-toggle="tooltip" title="${_('%s comments') % len(c.cs_comments[cs.raw_id])}">
126 ${len(c.cs_comments[cs.raw_id])}
127 127 <i class="icon-comment-discussion"></i>
128 128 </a>
129 129 %endif
130 130 %if cs.bumped:
131 131 <span class="bumpedtag" title="Bumped">
132 132 Bumped
133 133 </span>
134 134 %endif
135 135 %if cs.divergent:
136 136 <span class="divergenttag" title="Divergent">
137 137 Divergent
138 138 </span>
139 139 %endif
140 140 %if cs.extinct:
141 141 <span class="extincttag" title="Extinct">
142 142 Extinct
143 143 </span>
144 144 %endif
145 145 %if cs.unstable:
146 146 <span class="unstabletag" title="Unstable">
147 147 Unstable
148 148 </span>
149 149 %endif
150 150 %if cs.phase:
151 151 <span class="phasetag" title="Phase">
152 152 ${cs.phase}
153 153 </span>
154 154 %endif
155 155 %if h.is_hg(c.db_repo_scm_instance):
156 156 %for book in cs.bookmarks:
157 157 <span class="booktag" title="${_('Bookmark %s') % book}">
158 158 ${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
159 159 </span>
160 160 %endfor
161 161 %endif
162 162 %for tag in cs.tags:
163 163 <span class="tagtag" title="${_('Tag %s') % tag}">
164 164 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
165 165 </span>
166 166 %endfor
167 167 %if (not c.branch_name) and cs.branch:
168 168 <span class="branchtag" title="${_('Branch %s' % cs.branch)}">
169 169 ${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch))}
170 170 </span>
171 171 %endif
172 172 </div>
173 173 </div>
174 174 </td>
175 175 </tr>
176 176 %endfor
177 177 </tbody>
178 178 </table>
179 179
180 180 <input type="checkbox" id="singlerange" style="display:none"/>
181 181
182 182 </div>
183 183
184 184 ${c.pagination.pager()}
185 185
186 186 <script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
187 187 <script type="text/javascript">
188 188 var jsdata = ${h.js(c.jsdata)};
189 189 var graph = new BranchRenderer('graph_canvas', 'graph_content', 'chg_');
190 190
191 191 $(document).ready(function(){
192 192 var $checkboxes = $('.changeset_range');
193 193
194 194 pyroutes.register('changeset_home', ${h.js(h.url('changeset_home', repo_name='%(repo_name)s', revision='%(revision)s'))}, ['repo_name', 'revision']);
195 195
196 196 var checkbox_checker = function(e) {
197 197 var $checked_checkboxes = $checkboxes.filter(':checked');
198 198 var $singlerange = $('#singlerange');
199 199
200 200 $('#rev_range_container').hide();
201 201 $checkboxes.show();
202 202 $singlerange.show();
203 203
204 204 if ($checked_checkboxes.length > 0) {
205 205 $checked_checkboxes.first().parent('td').append($singlerange);
206 206 var singlerange = $singlerange.prop('checked');
207 207 var rev_end = $checked_checkboxes.first().prop('name');
208 208 if ($checked_checkboxes.length > 1 || singlerange) {
209 209 var rev_start = $checked_checkboxes.last().prop('name');
210 210 $('#rev_range_container').prop('href',
211 211 pyroutes.url('changeset_home', {'repo_name': ${h.js(c.repo_name)},
212 212 'revision': rev_start + '...' + rev_end}));
213 213 $('#rev_range_container').html(
214 214 _TM['Show Selected Changesets {0} &rarr; {1}'].format(rev_start.substr(0, 12), rev_end.substr(0, 12)));
215 215 $('#rev_range_container').show();
216 216 $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
217 217 {'repo_name': ${h.js(c.repo_name)},
218 218 'rev_start': rev_start,
219 219 'rev_end': rev_end}));
220 220 $('#open_new_pr').html(_TM['Open New Pull Request for {0} &rarr; {1}'].format(rev_start.substr(0, 12), rev_end.substr(0, 12)));
221 221 } else {
222 222 $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
223 223 {'repo_name': ${h.js(c.repo_name)},
224 224 'rev_end': rev_end}));
225 225 $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format(rev_end.substr(0, 12)));
226 226 }
227 227 $('#rev_range_clear').show();
228 228 $('#compare_fork').hide();
229 229
230 230 var disabled = true;
231 231 $checkboxes.each(function(){
232 232 var $this = $(this);
233 233 if (disabled) {
234 234 if ($this.prop('checked')) {
235 235 $this.closest('tr').removeClass('out-of-range');
236 236 disabled = singlerange;
237 237 } else {
238 238 $this.closest('tr').addClass('out-of-range');
239 239 }
240 240 } else {
241 241 $this.closest('tr').removeClass('out-of-range');
242 242 disabled = $this.prop('checked');
243 243 }
244 244 });
245 245
246 246 if ($checked_checkboxes.length + (singlerange ? 1 : 0) >= 2) {
247 247 $checkboxes.hide();
248 248 $checked_checkboxes.show();
249 249 if (!singlerange)
250 250 $singlerange.hide();
251 251 }
252 252 } else {
253 253 $('#singlerange').hide().prop('checked', false);
254 254 $('#rev_range_clear').hide();
255 255 %if c.revision:
256 256 $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
257 257 {'repo_name': ${h.js(c.repo_name)},
258 258 'rev_end':${h.js(c.first_revision.raw_id)}}));
259 259 $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format(${h.jshtml(c.revision)}));
260 260 %else:
261 261 $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
262 262 {'repo_name': ${h.js(c.repo_name)},
263 263 'branch':${h.js(c.first_revision.branch)}}));
264 264 $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format(${h.jshtml(c.first_revision.branch)}));
265 265 %endif
266 266 $('#compare_fork').show();
267 267 $checkboxes.closest('tr').removeClass('out-of-range');
268 268 }
269 269 };
270 270 checkbox_checker();
271 271 $checkboxes.click(function() {
272 272 checkbox_checker();
273 273 graph.render(jsdata);
274 274 });
275 275 $('#singlerange').click(checkbox_checker);
276 276
277 277 $('#rev_range_clear').click(function(e){
278 278 $checkboxes.prop('checked', false);
279 279 checkbox_checker();
280 280 graph.render(jsdata);
281 281 });
282 282
283 283 var $msgs = $('.message');
284 284 // get first element height
285 285 var el = $('#graph_content tr')[0];
286 286 var row_h = el.clientHeight;
287 287 $msgs.each(function() {
288 288 var m = this;
289 289
290 290 var h = m.clientHeight;
291 291 if(h > row_h){
292 292 var offset = row_h - (h+12);
293 293 $(m.nextElementSibling).css('display', 'block');
294 294 $(m.nextElementSibling).css('margin-top', offset+'px');
295 295 }
296 296 });
297 297
298 298 $('.expand_commit').on('click',function(e){
299 299 $(this).next('.mid').find('.message > div').toggleClass('hidden');
300 300
301 301 //redraw the graph, r and jsdata are bound outside function
302 302 graph.render(jsdata);
303 303 });
304 304
305 305 // change branch filter
306 306 $("#branch_filter").select2({
307 307 dropdownAutoWidth: true,
308 308 maxResults: 50,
309 309 sortResults: branchSort
310 310 });
311 311
312 312 $("#branch_filter").change(function(e){
313 313 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
314 314 if(selected_branch != ''){
315 315 window.location = pyroutes.url('changelog_home', {'repo_name': ${h.js(c.repo_name)},
316 316 'branch': selected_branch});
317 317 }else{
318 318 window.location = pyroutes.url('changelog_home', {'repo_name': ${h.js(c.repo_name)}});
319 319 }
320 320 $("#changelog").hide();
321 321 });
322 322
323 323 graph.render(jsdata);
324 324 });
325 325
326 326 $(window).resize(function(){
327 327 graph.render(jsdata);
328 328 });
329 329 </script>
330 330 %else:
331 331 ${_('There are no changes yet')}
332 332 %endif
333 333 </div>
334 334 </div>
335 335 </%def>
@@ -1,105 +1,105 b''
1 1 ## -*- coding: utf-8 -*-
2 2 %if c.repo_changesets:
3 3 <table class="table">
4 4 <tr>
5 5 <th class="left"></th>
6 6 <th class="left"></th>
7 7 <th class="left">${_('Revision')}</th>
8 8 <th class="left">${_('Commit Message')}</th>
9 9 <th class="left">${_('Age')}</th>
10 10 <th class="left">${_('Author')}</th>
11 11 <th class="left">${_('Refs')}</th>
12 12 </tr>
13 13 %for cnt,cs in enumerate(c.repo_changesets):
14 14 <tr class="parity${cnt%2} ${'mergerow' if len(cs.parents) > 1 else ''}">
15 15 <td class="compact">
16 %if c.statuses.get(cs.raw_id):
17 %if c.statuses.get(cs.raw_id)[2]:
18 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username, c.statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
19 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
16 %if c.cs_statuses.get(cs.raw_id):
17 %if c.cs_statuses.get(cs.raw_id)[2]:
18 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.cs_statuses.get(cs.raw_id)[1], c.cs_statuses.get(cs.raw_id)[5].username, c.cs_statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.cs_statuses.get(cs.raw_id)[3],pull_request_id=c.cs_statuses.get(cs.raw_id)[2])}">
19 <i class="icon-circle changeset-status-${c.cs_statuses.get(cs.raw_id)[0]}"></i>
20 20 </a>
21 21 %else:
22 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username)}"
23 href="${c.comments[cs.raw_id][0].url()}">
24 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
22 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.cs_statuses.get(cs.raw_id)[1], c.cs_statuses.get(cs.raw_id)[5].username)}"
23 href="${c.cs_comments[cs.raw_id][0].url()}">
24 <i class="icon-circle changeset-status-${c.cs_statuses.get(cs.raw_id)[0]}"></i>
25 25 </a>
26 26 %endif
27 27 %endif
28 28 </td>
29 29 <td class="compact">
30 %if c.comments.get(cs.raw_id,[]):
30 %if c.cs_comments.get(cs.raw_id,[]):
31 31 <div class="comments-container">
32 32 <div title="${('comments')}">
33 <a href="${c.comments[cs.raw_id][0].url()}">
34 <i class="icon-comment"></i>${len(c.comments[cs.raw_id])}
33 <a href="${c.cs_comments[cs.raw_id][0].url()}">
34 <i class="icon-comment"></i>${len(c.cs_comments[cs.raw_id])}
35 35 </a>
36 36 </div>
37 37 </div>
38 38 %endif
39 39 </td>
40 40 <td>
41 41 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}" class="revision-link">${h.show_id(cs)}</a>
42 42 </td>
43 43 <td>
44 44 ${h.urlify_text(h.chop_at(cs.message,'\n'),c.repo_name, h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
45 45 </td>
46 46 <td><span data-toggle="tooltip" title="${h.fmt_date(cs.date)}">
47 47 ${h.age(cs.date)}</span>
48 48 </td>
49 49 <td title="${cs.author}">${h.person(cs.author)}</td>
50 50 <td>
51 51 %if h.is_hg(c.db_repo_scm_instance):
52 52 %for book in cs.bookmarks:
53 53 <span class="booktag" title="${_('Bookmark %s') % book}">
54 54 ${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
55 55 </span>
56 56 %endfor
57 57 %endif
58 58 %for tag in cs.tags:
59 59 <span class="tagtag" title="${_('Tag %s') % tag}">
60 60 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
61 61 </span>
62 62 %endfor
63 63 %if cs.branch:
64 64 <span class="branchtag" title="${_('Branch %s' % cs.branch)}">
65 65 ${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch))}
66 66 </span>
67 67 %endif
68 68 </td>
69 69 </tr>
70 70 %endfor
71 71
72 72 </table>
73 73
74 74 ${c.repo_changesets.pager()}
75 75
76 76 %else:
77 77
78 78 %if h.HasRepoPermissionLevel('write')(c.repo_name):
79 79 <h4>${_('Add or upload files directly via Kallithea')}</h4>
80 80 <div>
81 81 <div id="add_node_id" class="add_node">
82 82 <a class="btn btn-default btn-xs" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}">${_('Add New File')}</a>
83 83 </div>
84 84 </div>
85 85 %endif
86 86
87 87
88 88 <h4>${_('Push new repository')}</h4>
89 89 <pre>
90 90 ${c.db_repo_scm_instance.alias} clone ${c.clone_repo_url}
91 91 ${c.db_repo_scm_instance.alias} add README # add first file
92 92 ${c.db_repo_scm_instance.alias} commit -m "Initial" # commit with message
93 93 ${c.db_repo_scm_instance.alias} push ${'origin master' if h.is_git(c.db_repo_scm_instance) else ''} # push changes back
94 94 </pre>
95 95
96 96 <h4>${_('Existing repository?')}</h4>
97 97 <pre>
98 98 %if h.is_git(c.db_repo_scm_instance):
99 99 git remote add origin ${c.clone_repo_url}
100 100 git push -u origin master
101 101 %else:
102 102 hg push ${c.clone_repo_url}
103 103 %endif
104 104 </pre>
105 105 %endif
@@ -1,143 +1,143 b''
1 1 ## Changesets table !
2 2 <div>
3 3 %if not c.cs_ranges:
4 4 <span class="empty_data">${_('No changesets')}</span>
5 5 %else:
6 6
7 7 %if c.ancestors:
8 8 <div class="ancestor">
9 9 %if len(c.ancestors) > 1:
10 10 <div class="text-danger">
11 11 ${_('Criss cross merge situation with multiple merge ancestors detected!')}
12 12 </div>
13 13 <div>
14 14 ${_('Please merge the target branch to your branch before creating a pull request.')}
15 15 </div>
16 16 %endif
17 17 <div>
18 18 ${_('Merge Ancestor')}:
19 19 %for ancestor in c.ancestors:
20 20 ${h.link_to(h.short_id(ancestor),h.url('changeset_home',repo_name=c.repo_name,revision=ancestor), class_="changeset_hash")}
21 21 %endfor
22 22 </div>
23 23 </div>
24 24 %endif
25 25
26 26 <div id="graph_nodes">
27 27 <canvas id="graph_canvas"></canvas>
28 28 </div>
29 29
30 30 <div id="graph_content_pr">
31 31
32 32 <table class="table compare_view_commits">
33 33 %for cnt, cs in enumerate(reversed(c.cs_ranges)):
34 34 <tr id="chg_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 else ''}">
35 35 <td>
36 %if cs.raw_id in c.statuses:
37 <i class="icon-circle changeset-status-${c.statuses[cs.raw_id][0]}" title="${_('Changeset status: %s') % c.statuses[cs.raw_id][1]}"></i>
36 %if cs.raw_id in c.cs_statuses:
37 <i class="icon-circle changeset-status-${c.cs_statuses[cs.raw_id][0]}" title="${_('Changeset status: %s') % c.cs_statuses[cs.raw_id][1]}"></i>
38 38 %endif
39 39 %if c.cs_comments.get(cs.raw_id):
40 40 <div class="comments-container">
41 41 <div class="comments-cnt" title="${_('Changeset has comments')}">
42 42 <a href="${c.cs_comments[cs.raw_id][0].url()}">
43 43 ${len(c.cs_comments[cs.raw_id])}
44 44 <i class="icon-comment"></i>
45 45 </a>
46 46 </div>
47 47 </div>
48 48 %endif
49 49 </td>
50 50 <td class="changeset-logical-index">
51 51 <%
52 52 num_cs = len(c.cs_ranges)
53 53 index = num_cs - cnt
54 54 if index == 1:
55 55 title = _('First (oldest) changeset in this list')
56 56 elif index == num_cs:
57 57 title = _('Last (most recent) changeset in this list')
58 58 else:
59 59 title = _('Position in this list of changesets')
60 60 %>
61 61 <span data-toggle="tooltip" title="${title}">
62 62 ${index}
63 63 </span>
64 64 </td>
65 65 <td><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
66 66 <td class="author">
67 67 ${h.gravatar(h.email_or_none(cs.author), size=16)}
68 68 <span data-toggle="tooltip" title="${cs.author}" class="user">${h.person(cs.author)}</span>
69 69 </td>
70 70 <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id), class_='changeset_hash')}</td>
71 71 <td>
72 72 %if cs.branch:
73 73 <span class="branchtag">${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.cs_repo.repo_name,branch=cs.branch))}</span>
74 74 %endif
75 75 </td>
76 76 <td class="expand_commit" title="${_('Expand commit message')}">
77 77 <i class="icon-align-left"></i>
78 78 </td>
79 79 <td class="mid">
80 80 <div class="pull-right">
81 81 %for tag in cs.tags:
82 82 <span class="tagtag" title="${_('Tag %s') % tag}">
83 83 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
84 84 </span>
85 85 %endfor
86 86 </div>
87 87 <div class="message">${h.urlify_text(cs.message, c.repo_name)}</div>
88 88 </td>
89 89 </tr>
90 90 %endfor
91 91 </table>
92 92
93 93 </div>
94 94
95 95 %if c.is_ajax_preview:
96 96 <h5>
97 97 ## links should perhaps use ('rev', c.a_rev) instead ...
98 98 ${h.link_to(_('Show merge diff'),
99 99 h.url('compare_url',
100 100 repo_name=c.a_repo.repo_name,
101 101 org_ref_type=c.a_ref_type, org_ref_name=c.a_ref_name,
102 102 other_repo=c.cs_repo.repo_name,
103 103 other_ref_type=c.cs_ref_type, other_ref_name=c.cs_ref_name,
104 104 merge='1')
105 105 )}
106 106 </h5>
107 107 %endif
108 108 %if c.cs_ranges_org is not None:
109 109 ## TODO: list actual changesets?
110 110 <div>
111 111 ${h.link_to_ref(c.cs_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
112 112 ${_('is')}
113 113 <a href="${c.swap_url}">${_('%s changesets') % (len(c.cs_ranges_org))}</a>
114 114 ${_('behind')}
115 115 ${h.link_to_ref(c.a_repo.repo_name, c.a_ref_type, c.a_ref_name)}
116 116 </div>
117 117 %endif
118 118 %endif
119 119 </div>
120 120
121 121 %if c.is_ajax_preview:
122 122 <div id="jsdata" style="display:none">${h.js(c.jsdata)}</div>
123 123 %else:
124 124 <script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
125 125 %endif
126 126
127 127 <script type="text/javascript">
128 128 var jsdata = ${h.js(c.jsdata)};
129 129 var graph = new BranchRenderer('graph_canvas', 'graph_content_pr', 'chg_');
130 130
131 131 $(document).ready(function(){
132 132 graph.render(jsdata);
133 133
134 134 $('.expand_commit').click(function(e){
135 135 $(this).next('.mid').find('.message').toggleClass('expanded');
136 136 graph.render(jsdata);
137 137 });
138 138 });
139 139 $(window).resize(function(){
140 140 graph.render(jsdata);
141 141 });
142 142
143 143 </script>
General Comments 0
You need to be logged in to leave comments. Login now