##// END OF EJS Templates
compare: add options for ignore whitespace and increase context
Mads Kiilerich -
r4309:60ae17de default
parent child Browse files
Show More
@@ -1,312 +1,321 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 for pylons 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 traceback
32 32 import re
33 33
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35 from pylons import request, response, session, tmpl_context as c, url
36 36 from pylons.controllers.util import abort, redirect
37 37 from pylons.i18n.translation import _
38 38
39 39 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
40 40 from kallithea.lib.vcs.utils import safe_str
41 41 from kallithea.lib.vcs.utils.hgcompat import unionrepo
42 42 from kallithea.lib import helpers as h
43 43 from kallithea.lib.base import BaseRepoController, render
44 44 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 45 from kallithea.lib import diffs
46 46 from kallithea.model.db import Repository
47 47 from kallithea.lib.diffs import LimitedDiffContainer
48
48 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
49 _context_url, get_line_ctx, get_ignore_ws
49 50
50 51 log = logging.getLogger(__name__)
51 52
52 53
53 54 class CompareController(BaseRepoController):
54 55
55 56 def __before__(self):
56 57 super(CompareController, self).__before__()
57 58
58 59 def __get_rev_or_redirect(self, ref, repo, redirect_after=True,
59 60 partial=False):
60 61 """
61 62 Safe way to get changeset if error occur it redirects to changeset with
62 63 proper message. If partial is set then don't do redirect raise Exception
63 64 instead
64 65
65 66 :param ref:
66 67 :param repo:
67 68 :param redirect_after:
68 69 :param partial:
69 70 """
70 71 rev = ref[1] # default and used for git
71 72 if repo.scm_instance.alias == 'hg':
72 73 # lookup up the exact node id
73 74 _revset_predicates = {
74 75 'branch': 'branch',
75 76 'book': 'bookmark',
76 77 'tag': 'tag',
77 78 'rev': 'id',
78 79 }
79 80 rev_spec = "max(%s(%%s))" % _revset_predicates[ref[0]]
80 81 revs = repo.scm_instance._repo.revs(rev_spec, safe_str(ref[1]))
81 82 if revs:
82 83 rev = revs[-1]
83 84 # else: TODO: just report 'not found'
84 85
85 86 try:
86 87 return repo.scm_instance.get_changeset(rev).raw_id
87 88 except EmptyRepositoryError, e:
88 89 if not redirect_after:
89 90 return None
90 91 h.flash(h.literal(_('There are no changesets yet')),
91 92 category='warning')
92 93 redirect(url('summary_home', repo_name=repo.repo_name))
93 94
94 95 except RepositoryError, e:
95 96 log.error(traceback.format_exc())
96 97 h.flash(safe_str(e), category='warning')
97 98 if not partial:
98 99 redirect(h.url('summary_home', repo_name=repo.repo_name))
99 100 raise HTTPBadRequest()
100 101
101 102 def _get_changesets(self, alias, org_repo, org_rev, other_repo, other_rev):
102 103 """
103 104 Returns lists of changesets that can be merged from org_repo@org_rev
104 105 to other_repo@other_rev
105 106 ... and the other way
106 107 ... and the ancestor that would be used for merge
107 108
108 109 :param org_repo: repo object, that is most likely the orginal repo we forked from
109 110 :param org_rev: the revision we want our compare to be made
110 111 :param other_repo: repo object, mostl likely the fork of org_repo. It hass
111 112 all changesets that we need to obtain
112 113 :param other_rev: revision we want out compare to be made on other_repo
113 114 """
114 115 ancestor = None
115 116 if org_rev == other_rev:
116 117 org_changesets = []
117 118 other_changesets = []
118 119 ancestor = org_rev
119 120
120 121 elif alias == 'hg':
121 122 #case two independent repos
122 123 if org_repo != other_repo:
123 124 hgrepo = unionrepo.unionrepository(other_repo.baseui,
124 125 other_repo.path,
125 126 org_repo.path)
126 127 # all ancestors of other_rev will be in other_repo and
127 128 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
128 129
129 130 #no remote compare do it on the same repository
130 131 else:
131 132 hgrepo = other_repo._repo
132 133
133 134 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
134 135 if ancestors:
135 136 # pick arbitrary ancestor - but there is usually only one
136 137 ancestor = hgrepo[ancestors[0]].hex()
137 138
138 139 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
139 140 other_rev, org_rev, org_rev)
140 141 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
141 142 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
142 143 org_rev, other_rev, other_rev)
143 144
144 145 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
145 146
146 147 elif alias == 'git':
147 148 if org_repo != other_repo:
148 149 from dulwich.repo import Repo
149 150 from dulwich.client import SubprocessGitClient
150 151
151 152 gitrepo = Repo(org_repo.path)
152 153 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
153 154
154 155 gitrepo_remote = Repo(other_repo.path)
155 156 SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote)
156 157
157 158 revs = []
158 159 for x in gitrepo_remote.get_walker(include=[other_rev],
159 160 exclude=[org_rev]):
160 161 revs.append(x.commit.id)
161 162
162 163 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
163 164 if other_changesets:
164 165 ancestor = other_changesets[0].parents[0].raw_id
165 166 else:
166 167 # no changesets from other repo, ancestor is the other_rev
167 168 ancestor = other_rev
168 169
169 170 else:
170 171 so, se = org_repo.run_git_command(
171 172 'log --reverse --pretty="format: %%H" -s %s..%s'
172 173 % (org_rev, other_rev)
173 174 )
174 175 other_changesets = [org_repo.get_changeset(cs)
175 176 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
176 177 org_changesets = []
177 178
178 179 else:
179 180 raise Exception('Bad alias only git and hg is allowed')
180 181
181 182 return other_changesets, org_changesets, ancestor
182 183
183 184 @LoginRequired()
184 185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 186 'repository.admin')
186 187 def index(self, repo_name):
187 188 c.compare_home = True
188 189 org_repo = c.db_repo.repo_name
189 190 other_repo = request.GET.get('other_repo', org_repo)
190 191 c.org_repo = Repository.get_by_repo_name(org_repo)
191 192 c.other_repo = Repository.get_by_repo_name(other_repo)
192 193 c.org_ref_name = c.other_ref_name = _('Select changeset')
193 194 return render('compare/compare_diff.html')
194 195
195 196 @LoginRequired()
196 197 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
197 198 'repository.admin')
198 199 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
199 200 # org_ref will be evaluated in org_repo
200 201 org_repo = c.db_repo.repo_name
201 202 org_ref = (org_ref_type, org_ref_name)
202 203 # other_ref will be evaluated in other_repo
203 204 other_ref = (other_ref_type, other_ref_name)
204 205 other_repo = request.GET.get('other_repo', org_repo)
205 206 # If merge is True:
206 207 # Show what org would get if merged with other:
207 208 # List changesets that are ancestors of other but not of org.
208 209 # New changesets in org is thus ignored.
209 210 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
210 211 # If merge is False:
211 212 # Make a raw diff from org to other, no matter if related or not.
212 213 # Changesets in one and not in the other will be ignored
213 214 merge = bool(request.GET.get('merge'))
214 215 # fulldiff disables cut_off_limit
215 216 c.fulldiff = request.GET.get('fulldiff')
216 217 # partial uses compare_cs.html template directly
217 218 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
218 219 # as_form puts hidden input field with changeset revisions
219 220 c.as_form = partial and request.GET.get('as_form')
220 221 # swap url for compare_diff page - never partial and never as_form
221 222 c.swap_url = h.url('compare_url',
222 223 repo_name=other_repo,
223 224 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
224 225 other_repo=org_repo,
225 226 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
226 227 merge=merge or '')
227 228
229 # set callbacks for generating markup for icons
230 c.ignorews_url = _ignorews_url
231 c.context_url = _context_url
232 ignore_whitespace = request.GET.get('ignorews') == '1'
233 line_context = request.GET.get('context', 3)
234
228 235 org_repo = Repository.get_by_repo_name(org_repo)
229 236 other_repo = Repository.get_by_repo_name(other_repo)
230 237
231 238 if org_repo is None:
232 239 msg = 'Could not find org repo %s' % org_repo
233 240 log.error(msg)
234 241 h.flash(msg, category='error')
235 242 return redirect(url('compare_home', repo_name=c.repo_name))
236 243
237 244 if other_repo is None:
238 245 msg = 'Could not find other repo %s' % other_repo
239 246 log.error(msg)
240 247 h.flash(msg, category='error')
241 248 return redirect(url('compare_home', repo_name=c.repo_name))
242 249
243 250 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
244 251 msg = 'compare of two different kind of remote repos not available'
245 252 log.error(msg)
246 253 h.flash(msg, category='error')
247 254 return redirect(url('compare_home', repo_name=c.repo_name))
248 255
249 256 c.org_rev = self.__get_rev_or_redirect(ref=org_ref, repo=org_repo, partial=partial)
250 257 c.other_rev = self.__get_rev_or_redirect(ref=other_ref, repo=other_repo, partial=partial)
251 258
252 259 c.compare_home = False
253 260 c.org_repo = org_repo
254 261 c.other_repo = other_repo
255 262 c.org_ref_name = org_ref_name
256 263 c.other_ref_name = other_ref_name
257 264 c.org_ref_type = org_ref_type
258 265 c.other_ref_type = other_ref_type
259 266
260 267 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
261 268 org_repo.scm_instance.alias, org_repo.scm_instance, c.org_rev,
262 269 other_repo.scm_instance, c.other_rev)
263 270 c.statuses = c.db_repo.statuses(
264 271 [x.raw_id for x in c.cs_ranges])
265 272
266 273 if partial:
267 274 return render('compare/compare_cs.html')
268 275 if merge and c.ancestor:
269 276 # case we want a simple diff without incoming changesets,
270 277 # previewing what will be merged.
271 278 # Make the diff on the other repo (which is known to have other_rev)
272 279 log.debug('Using ancestor %s as rev1 instead of %s'
273 280 % (c.ancestor, c.org_rev))
274 281 rev1 = c.ancestor
275 282 org_repo = other_repo
276 283 else: # comparing tips, not necessarily linearly related
277 284 if merge:
278 285 log.error('Unable to find ancestor revision')
279 286 if org_repo != other_repo:
280 287 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
281 288 raise HTTPNotFound
282 289 rev1 = c.org_rev
283 290
284 291 diff_limit = self.cut_off_limit if not c.fulldiff else None
285 292
286 293 log.debug('running diff between %s and %s in %s'
287 294 % (rev1, c.other_rev, org_repo.scm_instance.path))
288 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.other_rev)
295 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.other_rev,
296 ignore_whitespace=ignore_whitespace,
297 context=line_context)
289 298
290 299 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
291 300 diff_limit=diff_limit)
292 301 _parsed = diff_processor.prepare()
293 302
294 303 c.limited_diff = False
295 304 if isinstance(_parsed, LimitedDiffContainer):
296 305 c.limited_diff = True
297 306
298 307 c.files = []
299 308 c.changes = {}
300 309 c.lines_added = 0
301 310 c.lines_deleted = 0
302 311 for f in _parsed:
303 312 st = f['stats']
304 313 if not st['binary']:
305 314 c.lines_added += st['added']
306 315 c.lines_deleted += st['deleted']
307 316 fid = h.FID('', f['filename'])
308 317 c.files.append([fid, f['operation'], f['filename'], f['stats']])
309 318 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
310 319 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
311 320
312 321 return render('compare/compare_diff.html')
@@ -1,581 +1,589 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 webob.exc import HTTPNotFound, HTTPForbidden
33 33 from collections import defaultdict
34 34 from itertools import groupby
35 35
36 36 from pylons import request, tmpl_context as c, url
37 37 from pylons.controllers.util import redirect
38 38 from pylons.i18n.translation import _
39 39
40 40 from kallithea.lib.compat import json
41 41 from kallithea.lib.base import BaseRepoController, render
42 42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
43 43 NotAnonymous
44 44 from kallithea.lib.helpers import Page
45 45 from kallithea.lib import helpers as h
46 46 from kallithea.lib import diffs
47 47 from kallithea.lib.utils import action_logger, jsonify
48 48 from kallithea.lib.vcs.utils import safe_str
49 49 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
50 50 from kallithea.lib.diffs import LimitedDiffContainer
51 51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
52 52 PullRequestReviewers
53 53 from kallithea.model.pull_request import PullRequestModel
54 54 from kallithea.model.meta import Session
55 55 from kallithea.model.repo import RepoModel
56 56 from kallithea.model.comment import ChangesetCommentsModel
57 57 from kallithea.model.changeset_status import ChangesetStatusModel
58 58 from kallithea.model.forms import PullRequestForm
59 59 from kallithea.lib.utils2 import safe_int
60 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
61 _context_url, get_line_ctx, get_ignore_ws
60 62
61 63 log = logging.getLogger(__name__)
62 64
63 65
64 66 class PullrequestsController(BaseRepoController):
65 67
66 68 def __before__(self):
67 69 super(PullrequestsController, self).__before__()
68 70 repo_model = RepoModel()
69 71 c.users_array = repo_model.get_users_js()
70 72 c.user_groups_array = repo_model.get_user_groups_js()
71 73
72 74 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
73 75 """return a structure with repo's interesting changesets, suitable for
74 76 the selectors in pullrequest.html
75 77
76 78 rev: a revision that must be in the list somehow and selected by default
77 79 branch: a branch that must be in the list and selected by default - even if closed
78 80 branch_rev: a revision of which peers should be preferred and available."""
79 81 # list named branches that has been merged to this named branch - it should probably merge back
80 82 peers = []
81 83
82 84 if rev:
83 85 rev = safe_str(rev)
84 86
85 87 if branch:
86 88 branch = safe_str(branch)
87 89
88 90 if branch_rev:
89 91 branch_rev = safe_str(branch_rev)
90 92 # not restricting to merge() would also get branch point and be better
91 93 # (especially because it would get the branch point) ... but is currently too expensive
92 94 otherbranches = {}
93 95 for i in repo._repo.revs(
94 96 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)))",
95 97 branch_rev, branch_rev):
96 98 cs = repo.get_changeset(i)
97 99 otherbranches[cs.branch] = cs.raw_id
98 100 for abranch, node in otherbranches.iteritems():
99 101 selected = 'branch:%s:%s' % (abranch, node)
100 102 peers.append((selected, abranch))
101 103
102 104 selected = None
103 105
104 106 branches = []
105 107 for abranch, branchrev in repo.branches.iteritems():
106 108 n = 'branch:%s:%s' % (abranch, branchrev)
107 109 branches.append((n, abranch))
108 110 if rev == branchrev:
109 111 selected = n
110 112 if branch == abranch:
111 113 selected = n
112 114 branch = None
113 115
114 116 if branch: # branch not in list - it is probably closed
115 117 revs = repo._repo.revs('max(branch(%s))', branch)
116 118 if revs:
117 119 cs = repo.get_changeset(revs[0])
118 120 selected = 'branch:%s:%s' % (branch, cs.raw_id)
119 121 branches.append((selected, branch))
120 122
121 123 bookmarks = []
122 124 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
123 125 n = 'book:%s:%s' % (bookmark, bookmarkrev)
124 126 bookmarks.append((n, bookmark))
125 127 if rev == bookmarkrev:
126 128 selected = n
127 129
128 130 tags = []
129 131 for tag, tagrev in repo.tags.iteritems():
130 132 n = 'tag:%s:%s' % (tag, tagrev)
131 133 tags.append((n, tag))
132 134 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
133 135 selected = n
134 136
135 137 # prio 1: rev was selected as existing entry above
136 138
137 139 # prio 2: create special entry for rev; rev _must_ be used
138 140 specials = []
139 141 if rev and selected is None:
140 142 selected = 'rev:%s:%s' % (rev, rev)
141 143 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
142 144
143 145 # prio 3: most recent peer branch
144 146 if peers and not selected:
145 147 selected = peers[0][0][0]
146 148
147 149 # prio 4: tip revision
148 150 if not selected:
149 151 if h.is_hg(repo):
150 152 if 'tip' in repo.tags:
151 153 selected = 'tag:tip:%s' % repo.tags['tip']
152 154 else:
153 155 selected = 'tag:null:0'
154 156 tags.append((selected, 'null'))
155 157 else:
156 158 if 'master' in repo.branches:
157 159 selected = 'branch:master:%s' % repo.branches['master']
158 160 else:
159 161 k, v = repo.branches.items()[0]
160 162 selected = 'branch:%s:%s' % (k, v)
161 163
162 164 groups = [(specials, _("Special")),
163 165 (peers, _("Peer branches")),
164 166 (bookmarks, _("Bookmarks")),
165 167 (branches, _("Branches")),
166 168 (tags, _("Tags")),
167 169 ]
168 170 return [g for g in groups if g[0]], selected
169 171
170 172 def _get_is_allowed_change_status(self, pull_request):
171 173 owner = self.authuser.user_id == pull_request.user_id
172 174 reviewer = self.authuser.user_id in [x.user_id for x in
173 175 pull_request.reviewers]
174 176 return self.authuser.admin or owner or reviewer
175 177
176 178 def _load_compare_data(self, pull_request, enable_comments=True):
177 179 """
178 180 Load context data needed for generating compare diff
179 181
180 182 :param pull_request:
181 183 """
182 184 c.org_repo = pull_request.org_repo
183 185 (c.org_ref_type,
184 186 c.org_ref_name,
185 187 c.org_rev) = pull_request.org_ref.split(':')
186 188
187 189 c.other_repo = c.org_repo
188 190 (c.other_ref_type,
189 191 c.other_ref_name,
190 192 c.other_rev) = pull_request.other_ref.split(':')
191 193
192 194 c.cs_ranges = [c.org_repo.get_changeset(x) for x in pull_request.revisions]
193 195 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
194 196
195 197 c.statuses = c.org_repo.statuses([x.raw_id for x in c.cs_ranges])
196 198
199 ignore_whitespace = request.GET.get('ignorews') == '1'
200 line_context = request.GET.get('context', 3)
201 c.ignorews_url = _ignorews_url
202 c.context_url = _context_url
197 203 c.fulldiff = request.GET.get('fulldiff')
198 204 diff_limit = self.cut_off_limit if not c.fulldiff else None
199 205
200 206 # we swap org/other ref since we run a simple diff on one repo
201 207 log.debug('running diff between %s and %s in %s'
202 208 % (c.other_rev, c.org_rev, c.org_repo.scm_instance.path))
203 txtdiff = c.org_repo.scm_instance.get_diff(rev1=safe_str(c.other_rev), rev2=safe_str(c.org_rev))
209 txtdiff = c.org_repo.scm_instance.get_diff(rev1=safe_str(c.other_rev), rev2=safe_str(c.org_rev),
210 ignore_whitespace=ignore_whitespace,
211 context=line_context)
204 212
205 213 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
206 214 diff_limit=diff_limit)
207 215 _parsed = diff_processor.prepare()
208 216
209 217 c.limited_diff = False
210 218 if isinstance(_parsed, LimitedDiffContainer):
211 219 c.limited_diff = True
212 220
213 221 c.files = []
214 222 c.changes = {}
215 223 c.lines_added = 0
216 224 c.lines_deleted = 0
217 225
218 226 for f in _parsed:
219 227 st = f['stats']
220 228 c.lines_added += st['added']
221 229 c.lines_deleted += st['deleted']
222 230 fid = h.FID('', f['filename'])
223 231 c.files.append([fid, f['operation'], f['filename'], f['stats']])
224 232 htmldiff = diff_processor.as_html(enable_comments=enable_comments,
225 233 parsed_lines=[f])
226 234 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
227 235
228 236 @LoginRequired()
229 237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
230 238 'repository.admin')
231 239 def show_all(self, repo_name):
232 240 c.from_ = request.GET.get('from_') or ''
233 241 c.closed = request.GET.get('closed') or ''
234 242 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
235 243 c.repo_name = repo_name
236 244 p = safe_int(request.GET.get('page', 1), 1)
237 245
238 246 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
239 247
240 248 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
241 249
242 250 if request.environ.get('HTTP_X_PARTIAL_XHR'):
243 251 return c.pullrequest_data
244 252
245 253 return render('/pullrequests/pullrequest_show_all.html')
246 254
247 255 @LoginRequired()
248 256 def show_my(self): # my_account_my_pullrequests
249 257 c.show_closed = request.GET.get('pr_show_closed')
250 258 return render('/pullrequests/pullrequest_show_my.html')
251 259
252 260 @NotAnonymous()
253 261 def show_my_data(self):
254 262 c.show_closed = request.GET.get('pr_show_closed')
255 263
256 264 def _filter(pr):
257 265 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
258 266 if not c.show_closed:
259 267 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
260 268 return s
261 269
262 270 c.my_pull_requests = _filter(PullRequest.query()\
263 271 .filter(PullRequest.user_id ==
264 272 self.authuser.user_id)\
265 273 .all())
266 274
267 275 c.participate_in_pull_requests = _filter(PullRequest.query()\
268 276 .join(PullRequestReviewers)\
269 277 .filter(PullRequestReviewers.user_id ==
270 278 self.authuser.user_id)\
271 279 )
272 280
273 281 return render('/pullrequests/pullrequest_show_my_data.html')
274 282
275 283 @LoginRequired()
276 284 @NotAnonymous()
277 285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
278 286 'repository.admin')
279 287 def index(self):
280 288 org_repo = c.db_repo
281 289
282 290 try:
283 291 org_repo.scm_instance.get_changeset()
284 292 except EmptyRepositoryError, e:
285 293 h.flash(h.literal(_('There are no changesets yet')),
286 294 category='warning')
287 295 redirect(url('summary_home', repo_name=org_repo.repo_name))
288 296
289 297 org_rev = request.GET.get('rev_end')
290 298 # rev_start is not directly useful - its parent could however be used
291 299 # as default for other and thus give a simple compare view
292 300 #other_rev = request.POST.get('rev_start')
293 301 branch = request.GET.get('branch')
294 302
295 303 c.org_repos = []
296 304 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
297 305 c.default_org_repo = org_repo.repo_name
298 306 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, rev=org_rev, branch=branch)
299 307
300 308 c.other_repos = []
301 309 other_repos_info = {}
302 310
303 311 def add_other_repo(repo, branch_rev=None):
304 312 if repo.repo_name in other_repos_info: # shouldn't happen
305 313 return
306 314 c.other_repos.append((repo.repo_name, repo.repo_name))
307 315 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
308 316 other_repos_info[repo.repo_name] = {
309 317 'user': dict(user_id=repo.user.user_id,
310 318 username=repo.user.username,
311 319 firstname=repo.user.firstname,
312 320 lastname=repo.user.lastname,
313 321 gravatar_link=h.gravatar_url(repo.user.email, 14)),
314 322 'description': repo.description.split('\n', 1)[0],
315 323 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
316 324 }
317 325
318 326 # add org repo to other so we can open pull request against peer branches on itself
319 327 add_other_repo(org_repo, branch_rev=org_rev)
320 328 c.default_other_repo = org_repo.repo_name
321 329
322 330 # gather forks and add to this list ... even though it is rare to
323 331 # request forks to pull from their parent
324 332 for fork in org_repo.forks:
325 333 add_other_repo(fork)
326 334
327 335 # add parents of this fork also, but only if it's not empty
328 336 if org_repo.parent and org_repo.parent.scm_instance.revisions:
329 337 add_other_repo(org_repo.parent)
330 338 c.default_other_repo = org_repo.parent.repo_name
331 339
332 340 c.default_other_repo_info = other_repos_info[c.default_other_repo]
333 341 c.other_repos_info = json.dumps(other_repos_info)
334 342
335 343 return render('/pullrequests/pullrequest.html')
336 344
337 345 @LoginRequired()
338 346 @NotAnonymous()
339 347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 348 'repository.admin')
341 349 def create(self, repo_name):
342 350 repo = RepoModel()._get_repo(repo_name)
343 351 try:
344 352 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
345 353 except formencode.Invalid, errors:
346 354 log.error(traceback.format_exc())
347 355 if errors.error_dict.get('revisions'):
348 356 msg = 'Revisions: %s' % errors.error_dict['revisions']
349 357 elif errors.error_dict.get('pullrequest_title'):
350 358 msg = _('Pull request requires a title with min. 3 chars')
351 359 else:
352 360 msg = _('Error creating pull request: %s') % errors.msg
353 361
354 362 h.flash(msg, 'error')
355 363 return redirect(url('pullrequest_home', repo_name=repo_name)) ## would rather just go back to form ...
356 364
357 365 org_repo = _form['org_repo']
358 366 org_ref = _form['org_ref'] # will end with merge_rev but have symbolic name
359 367 other_repo = _form['other_repo']
360 368 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev'] # could be calculated from other_ref ...
361 369 revisions = [x for x in reversed(_form['revisions'])]
362 370 reviewers = _form['review_members']
363 371
364 372 title = _form['pullrequest_title']
365 373 if not title:
366 374 title = '%s#%s to %s' % (org_repo, org_ref.split(':', 2)[1], other_repo)
367 375 description = _form['pullrequest_desc']
368 376 try:
369 377 pull_request = PullRequestModel().create(
370 378 self.authuser.user_id, org_repo, org_ref, other_repo,
371 379 other_ref, revisions, reviewers, title, description
372 380 )
373 381 Session().commit()
374 382 h.flash(_('Successfully opened new pull request'),
375 383 category='success')
376 384 except Exception:
377 385 h.flash(_('Error occurred while creating pull request'),
378 386 category='error')
379 387 log.error(traceback.format_exc())
380 388 return redirect(url('pullrequest_home', repo_name=repo_name))
381 389
382 390 return redirect(url('pullrequest_show', repo_name=other_repo,
383 391 pull_request_id=pull_request.pull_request_id))
384 392
385 393 @LoginRequired()
386 394 @NotAnonymous()
387 395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
388 396 'repository.admin')
389 397 @jsonify
390 398 def update(self, repo_name, pull_request_id):
391 399 pull_request = PullRequest.get_or_404(pull_request_id)
392 400 if pull_request.is_closed():
393 401 raise HTTPForbidden()
394 402 #only owner or admin can update it
395 403 owner = pull_request.author.user_id == c.authuser.user_id
396 404 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
397 405 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
398 406 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
399 407 request.POST.get('reviewers_ids', '').split(',')))
400 408
401 409 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
402 410 Session().commit()
403 411 return True
404 412 raise HTTPForbidden()
405 413
406 414 @LoginRequired()
407 415 @NotAnonymous()
408 416 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
409 417 'repository.admin')
410 418 @jsonify
411 419 def delete(self, repo_name, pull_request_id):
412 420 pull_request = PullRequest.get_or_404(pull_request_id)
413 421 #only owner can delete it !
414 422 if pull_request.author.user_id == c.authuser.user_id:
415 423 PullRequestModel().delete(pull_request)
416 424 Session().commit()
417 425 h.flash(_('Successfully deleted pull request'),
418 426 category='success')
419 427 return redirect(url('my_account_pullrequests'))
420 428 raise HTTPForbidden()
421 429
422 430 @LoginRequired()
423 431 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
424 432 'repository.admin')
425 433 def show(self, repo_name, pull_request_id):
426 434 repo_model = RepoModel()
427 435 c.users_array = repo_model.get_users_js()
428 436 c.user_groups_array = repo_model.get_user_groups_js()
429 437 c.pull_request = PullRequest.get_or_404(pull_request_id)
430 438 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
431 439 cc_model = ChangesetCommentsModel()
432 440 cs_model = ChangesetStatusModel()
433 441 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
434 442 pull_request=c.pull_request,
435 443 with_revisions=True)
436 444
437 445 cs_statuses = defaultdict(list)
438 446 for st in _cs_statuses:
439 447 cs_statuses[st.author.username] += [st]
440 448
441 449 c.pull_request_reviewers = []
442 450 c.pull_request_pending_reviewers = []
443 451 for o in c.pull_request.reviewers:
444 452 st = cs_statuses.get(o.user.username, None)
445 453 if st:
446 454 sorter = lambda k: k.version
447 455 st = [(x, list(y)[0])
448 456 for x, y in (groupby(sorted(st, key=sorter), sorter))]
449 457 else:
450 458 c.pull_request_pending_reviewers.append(o.user)
451 459 c.pull_request_reviewers.append([o.user, st])
452 460
453 461 # pull_requests repo_name we opened it against
454 462 # ie. other_repo must match
455 463 if repo_name != c.pull_request.other_repo.repo_name:
456 464 raise HTTPNotFound
457 465
458 466 # load compare data into template context
459 467 enable_comments = not c.pull_request.is_closed()
460 468 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
461 469
462 470 # inline comments
463 471 c.inline_cnt = 0
464 472 c.inline_comments = cc_model.get_inline_comments(
465 473 c.db_repo.repo_id,
466 474 pull_request=pull_request_id)
467 475 # count inline comments
468 476 for __, lines in c.inline_comments:
469 477 for comments in lines.values():
470 478 c.inline_cnt += len(comments)
471 479 # comments
472 480 c.comments = cc_model.get_comments(c.db_repo.repo_id,
473 481 pull_request=pull_request_id)
474 482
475 483 # (badly named) pull-request status calculation based on reviewer votes
476 484 c.current_changeset_status = cs_model.calculate_status(
477 485 c.pull_request_reviewers,
478 486 )
479 487 c.changeset_statuses = ChangesetStatus.STATUSES
480 488
481 489 c.as_form = False
482 490 c.ancestor = None # there is one - but right here we don't know which
483 491 return render('/pullrequests/pullrequest_show.html')
484 492
485 493 @LoginRequired()
486 494 @NotAnonymous()
487 495 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
488 496 'repository.admin')
489 497 @jsonify
490 498 def comment(self, repo_name, pull_request_id):
491 499 pull_request = PullRequest.get_or_404(pull_request_id)
492 500 if pull_request.is_closed():
493 501 raise HTTPForbidden()
494 502
495 503 status = request.POST.get('changeset_status')
496 504 change_status = request.POST.get('change_changeset_status')
497 505 text = request.POST.get('text')
498 506 close_pr = request.POST.get('save_close')
499 507
500 508 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
501 509 if status and change_status and allowed_to_change_status:
502 510 _def = (_('Status change -> %s')
503 511 % ChangesetStatus.get_status_lbl(status))
504 512 if close_pr:
505 513 _def = _('Closing with') + ' ' + _def
506 514 text = text or _def
507 515 comm = ChangesetCommentsModel().create(
508 516 text=text,
509 517 repo=c.db_repo.repo_id,
510 518 user=c.authuser.user_id,
511 519 pull_request=pull_request_id,
512 520 f_path=request.POST.get('f_path'),
513 521 line_no=request.POST.get('line'),
514 522 status_change=(ChangesetStatus.get_status_lbl(status)
515 523 if status and change_status
516 524 and allowed_to_change_status else None),
517 525 closing_pr=close_pr
518 526 )
519 527
520 528 action_logger(self.authuser,
521 529 'user_commented_pull_request:%s' % pull_request_id,
522 530 c.db_repo, self.ip_addr, self.sa)
523 531
524 532 if allowed_to_change_status:
525 533 # get status if set !
526 534 if status and change_status:
527 535 ChangesetStatusModel().set_status(
528 536 c.db_repo.repo_id,
529 537 status,
530 538 c.authuser.user_id,
531 539 comm,
532 540 pull_request=pull_request_id
533 541 )
534 542
535 543 if close_pr:
536 544 if status in ['rejected', 'approved']:
537 545 PullRequestModel().close_pull_request(pull_request_id)
538 546 action_logger(self.authuser,
539 547 'user_closed_pull_request:%s' % pull_request_id,
540 548 c.db_repo, self.ip_addr, self.sa)
541 549 else:
542 550 h.flash(_('Closing pull request on other statuses than '
543 551 'rejected or approved forbidden'),
544 552 category='warning')
545 553
546 554 Session().commit()
547 555
548 556 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
549 557 return redirect(h.url('pullrequest_show', repo_name=repo_name,
550 558 pull_request_id=pull_request_id))
551 559
552 560 data = {
553 561 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
554 562 }
555 563 if comm:
556 564 c.co = comm
557 565 data.update(comm.get_dict())
558 566 data.update({'rendered_text':
559 567 render('changeset/changeset_comment_block.html')})
560 568
561 569 return data
562 570
563 571 @LoginRequired()
564 572 @NotAnonymous()
565 573 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
566 574 'repository.admin')
567 575 @jsonify
568 576 def delete_comment(self, repo_name, comment_id):
569 577 co = ChangesetComment.get(comment_id)
570 578 if co.pull_request.is_closed():
571 579 #don't allow deleting comments on closed pull request
572 580 raise HTTPForbidden()
573 581
574 582 owner = co.author.user_id == c.authuser.user_id
575 583 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
576 584 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
577 585 ChangesetCommentsModel().delete(comment=co)
578 586 Session().commit()
579 587 return True
580 588 else:
581 589 raise HTTPForbidden()
@@ -1,92 +1,94 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##usage:
3 3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 4 ## ${diff_block.diff_block(change)}
5 5 ##
6 6 <%def name="diff_block(change)">
7 7 <div class="diff-collapse">
8 8 <span target="${'diff-container-%s' % (id(change))}" class="diff-collapse-button">&uarr; ${_('Collapse diff')} &uarr;</span>
9 9 </div>
10 10 <div class="diff-container" id="${'diff-container-%s' % (id(change))}">
11 11 %for FID,(cs1, cs2, change, path, diff, stats) in change.iteritems():
12 12 <div id="${FID}_target" style="clear:both;margin-top:25px"></div>
13 13 <div id="${FID}" class="diffblock margined comm">
14 14 <div class="code-header">
15 15 <div class="changeset_header">
16 16 <div class="changeset_file">
17 17 ${h.link_to_if(change!='D',h.safe_unicode(path),h.url('files_home',repo_name=c.repo_name,
18 18 revision=cs2,f_path=h.safe_unicode(path)))}
19 19 </div>
20 20 <div class="diff-actions">
21 21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full diff for this file'))}">
22 22 <img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/>
23 23 </a>
24 24 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
25 25 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
26 26 </a>
27 27 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('Raw diff'))}">
28 28 <img class="icon" src="${h.url('/images/icons/page_white.png')}"/>
29 29 </a>
30 30 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('Download diff'))}">
31 31 <img class="icon" src="${h.url('/images/icons/page_save.png')}"/>
32 32 </a>
33 33 ${c.ignorews_url(request.GET, h.FID(cs2,path))}
34 34 ${c.context_url(request.GET, h.FID(cs2,path))}
35 35 </div>
36 36 <span style="float:right;margin-top:-3px">
37 37 <label>
38 38 ${_('Show inline comments')}
39 39 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(cs2,path))}
40 40 </label>
41 41 </span>
42 42 </div>
43 43 </div>
44 44 <div class="code-body">
45 45 <div class="full_f_path" path="${h.safe_unicode(path)}"></div>
46 46 ${diff|n}
47 47 </div>
48 48 </div>
49 49 %endfor
50 50 </div>
51 51 </%def>
52 52
53 53 <%def name="diff_block_simple(change)">
54 54
55 55 %for op,filenode_path,diff in change:
56 56 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
57 57 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
58 58 <div class="code-header">
59 59 <div class="changeset_header">
60 60 <div class="changeset_file">
61 61 ${h.safe_unicode(filenode_path)} |
62 62 ## TODO: link to ancestor and head of other instead of exactly other
63 63 %if op == 'A':
64 64 ${_('Added')}
65 65 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
66 66 %elif op == 'M':
67 67 <a class="spantag" href="${h.url('files_home', repo_name=c.org_repo.repo_name, f_path=filenode_path, revision=c.org_rev)}">${h.short_id(c.org_ref_name) if c.org_ref_type=='rev' else c.org_ref_name}</a>
68 68 <i class="icon-arrow-right"></i>
69 69 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
70 70 %elif op == 'D':
71 71 ${_('Deleted')}
72 72 <a class="spantag" href="${h.url('files_home', repo_name=c.other_repo.repo_name, f_path=filenode_path, revision=c.other_rev)}">${h.short_id(c.other_ref_name) if c.other_ref_type=='rev' else c.other_ref_name}</a>
73 73 %else:
74 74 ${op}???
75 75 %endif
76 76 </div>
77 <div class="diff-actions">
77 <div class="diff-actions">
78 78 %if c.other_repo.repo_name == c.repo_name:
79 79 <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode_path),diff2=c.other_rev,diff1=c.org_rev,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('Show full side-by-side diff for this file'))}">
80 80 <img class="icon" src="${h.url('/images/icons/application_double.png')}"/>
81 81 </a>
82 82 %endif
83 </div>
83 ${c.ignorews_url(request.GET)}
84 ${c.context_url(request.GET)}
85 </div>
84 86 </div>
85 87 </div>
86 88 <div class="code-body">
87 89 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
88 90 ${diff|n}
89 91 </div>
90 92 </div>
91 93 %endfor
92 94 </%def>
@@ -1,205 +1,208 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 %if c.compare_home:
6 6 ${_('%s Compare') % c.repo_name}
7 7 %else:
8 8 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)} &gt; ${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}
9 9 %endif
10 10 %if c.site_name:
11 11 &middot; ${c.site_name}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 ${_('Compare revisions')}
17 17 </%def>
18 18
19 19 <%def name="page_nav()">
20 20 ${self.menu('repositories')}
21 21 </%def>
22 22
23 23 <%def name="main()">
24 24 ${self.repo_context_bar('changelog')}
25 25 <div class="box">
26 26 <!-- box / title -->
27 27 <div class="title">
28 28 ${self.breadcrumbs()}
29 29 </div>
30 30 <div class="table">
31 31 <div id="body" class="diffblock">
32 32 <div class="code-header">
33 33 <div>
34 34 ${h.hidden('compare_org')} <i class="icon-ellipsis-horizontal" style="color: #999; vertical-align: -12px; padding: 0px 0px 0px 2px"></i> ${h.hidden('compare_other')}
35 35 %if not c.compare_home:
36 36 <a class="btn btn-small" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a>
37 37 %endif
38 38 <div id="compare_revs" class="btn btn-small"><i class="icon-loop"></i> ${_('Compare Revisions')}</div>
39 39 </div>
40 40 </div>
41 41 </div>
42 42
43 43 %if c.compare_home:
44 44 <div id="changeset_compare_view_content">
45 45 <div style="color:#999;font-size: 18px">${_('Compare revisions, branches, bookmarks or tags.')}</div>
46 46 </div>
47 47 %else:
48 48 <div id="changeset_compare_view_content">
49 49 ##CS
50 50 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
51 51 <%include file="compare_cs.html" />
52 52
53 53 ## FILES
54 54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
55 55
56 56 % if c.limited_diff:
57 57 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
58 58 % else:
59 59 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
60 60 %endif
61 61
62 ${c.ignorews_url(request.GET)}
63 ${c.context_url(request.GET)}
64
62 65 </div>
63 66 <div class="cs_files">
64 67 %if not c.files:
65 68 <span class="empty_data">${_('No files')}</span>
66 69 %endif
67 70 %for fid, change, f, stat in c.files:
68 71 <div class="cs_${change}">
69 72 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid, **request.GET.mixed()))}</div>
70 73 <div class="changes">${h.fancy_file_stats(stat)}</div>
71 74 </div>
72 75 %endfor
73 76 </div>
74 77 % if c.limited_diff:
75 78 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h5>
76 79 % endif
77 80 </div>
78 81
79 82 ## diff block
80 83 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
81 84 %for fid, change, f, stat in c.files:
82 85 ${diff_block.diff_block_simple([c.changes[fid]])}
83 86 %endfor
84 87 % if c.limited_diff:
85 88 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff')}</a></h4>
86 89 % endif
87 90 %endif
88 91 </div>
89 92
90 93 </div>
91 94 <script type="text/javascript">
92 95
93 96 $(document).ready(function(){
94 97 var cache = {}
95 98 $("#compare_org").select2({
96 99 placeholder: "${'%s@%s' % (c.org_repo.repo_name, c.org_ref_name)}",
97 100 formatSelection: function(obj){
98 101 return '{0}@{1}'.format("${c.org_repo.repo_name}", obj.text)
99 102 },
100 103 dropdownAutoWidth: true,
101 104 query: function(query){
102 105 var key = 'cache';
103 106 var cached = cache[key] ;
104 107 if(cached) {
105 108 var data = {results: []};
106 109 //filter results
107 110 $.each(cached.results, function(){
108 111 var section = this.text;
109 112 var children = [];
110 113 $.each(this.children, function(){
111 114 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
112 115 children.push({'id': this.id, 'text': this.text})
113 116 }
114 117 })
115 118 data.results.push({'text': section, 'children': children})
116 119 });
117 120 //push the typed in changeset
118 121 data.results.push({'text':_TM['Specify changeset'],
119 122 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
120 123 query.callback(data);
121 124 }else{
122 125 $.ajax({
123 126 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.org_repo.repo_name}'}),
124 127 data: {},
125 128 dataType: 'json',
126 129 type: 'GET',
127 130 success: function(data) {
128 131 cache[key] = data;
129 132 query.callback(data);
130 133 }
131 134 })
132 135 }
133 136 },
134 137 });
135 138 $("#compare_other").select2({
136 139 placeholder: "${'%s@%s' % (c.other_repo.repo_name, c.other_ref_name)}",
137 140 dropdownAutoWidth: true,
138 141 formatSelection: function(obj){
139 142 return '{0}@{1}'.format("${c.other_repo.repo_name}", obj.text)
140 143 },
141 144 query: function(query){
142 145 var key = 'cache2';
143 146 var cached = cache[key] ;
144 147 if(cached) {
145 148 var data = {results: []};
146 149 //filter results
147 150 $.each(cached.results, function(){
148 151 var section = this.text;
149 152 var children = [];
150 153 $.each(this.children, function(){
151 154 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
152 155 children.push({'id': this.id, 'text': this.text})
153 156 }
154 157 })
155 158 data.results.push({'text': section, 'children': children})
156 159 });
157 160 //push the typed in changeset
158 161 data.results.push({'text':_TM['Specify changeset'],
159 162 'children': [{'id': query.term, 'text': query.term, 'type': 'rev'}]})
160 163 query.callback(data);
161 164 }else{
162 165 $.ajax({
163 166 url: pyroutes.url('repo_refs_data', {'repo_name': '${c.other_repo.repo_name}'}),
164 167 data: {},
165 168 dataType: 'json',
166 169 type: 'GET',
167 170 success: function(data) {
168 171 cache[key] = data;
169 172 query.callback({results: data.results});
170 173 }
171 174 })
172 175 }
173 176 },
174 177 });
175 178
176 179 var values_changed = function() {
177 180 var values = $('#compare_org').select2('data') && $('#compare_other').select2('data');
178 181 if (values) {
179 182 $('#compare_revs').removeClass("disabled");
180 183 // TODO: the swap button ... if any
181 184 } else {
182 185 $('#compare_revs').addClass("disabled");
183 186 // TODO: the swap button ... if any
184 187 }
185 188 }
186 189 values_changed();
187 190 $('#compare_org').change(values_changed);
188 191 $('#compare_other').change(values_changed);
189 192 $('#compare_revs').on('click', function(e){
190 193 var org = $('#compare_org').select2('data');
191 194 var other = $('#compare_other').select2('data');
192 195 if (!org || !other) {
193 196 return;
194 197 }
195 198
196 199 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='__other_ref_type__',org_ref_name='__org__',other_ref_type='__org_ref_type__',other_ref_name='__other__', other_repo=c.other_repo.repo_name)}";
197 200 var u = compare_url.replace('__other_ref_type__',org.type)
198 201 .replace('__org__',org.text)
199 202 .replace('__org_ref_type__',other.type)
200 203 .replace('__other__',other.text);
201 204 window.location = u;
202 205 });
203 206 });
204 207 </script>
205 208 </%def>
@@ -1,269 +1,271 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.site_name:
6 6 &middot; ${c.site_name}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 ${_('Pull request #%s') % c.pull_request.pull_request_id}
12 12 </%def>
13 13
14 14 <%def name="page_nav()">
15 15 ${self.menu('repositories')}
16 16 </%def>
17 17
18 18 <%def name="main()">
19 19 ${self.repo_context_bar('showpullrequest')}
20 20 <div class="box">
21 21 <!-- box / title -->
22 22 <div class="title">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 <div class="form pr-box" style="float: left">
27 27 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
28 28 ${_('Title')}: ${c.pull_request.title}
29 29 %if c.pull_request.is_closed():
30 30 (${_('Closed')})
31 31 %endif
32 32 </div>
33 33 <div id="summary" class="fields">
34 34 <div class="field">
35 35 <div class="label-summary">
36 36 <label>${_('Review status')}:</label>
37 37 </div>
38 38 <div class="input">
39 39 <div class="changeset-status-container" style="float:none;clear:both">
40 40 %if c.current_changeset_status:
41 41 <div class="changeset-status-ico" style="padding:0px 4px 0px 0px">
42 42 <img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" title="${_('Pull request status calculated from votes')}"/></div>
43 43 <div class="changeset-status-lbl tooltip" title="${_('Pull request status calculated from votes')}">
44 44 %if c.pull_request.is_closed():
45 45 ${_('Closed')},
46 46 %endif
47 47 ${h.changeset_status_lbl(c.current_changeset_status)}
48 48 </div>
49 49
50 50 %endif
51 51 </div>
52 52 </div>
53 53 </div>
54 54 <div class="field">
55 55 <div class="label-summary">
56 56 <label>${_('Still not reviewed by')}:</label>
57 57 </div>
58 58 <div class="input">
59 59 % if len(c.pull_request_pending_reviewers) > 0:
60 60 <div class="tooltip" title="${h.tooltip(', '.join([x.username for x in c.pull_request_pending_reviewers]))}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
61 61 %else:
62 62 <div>${_('Pull request was reviewed by all reviewers')}</div>
63 63 %endif
64 64 </div>
65 65 </div>
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Origin repository')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div>
72 72 <span><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name)}">${c.pull_request.org_repo.clone_url()}</a></span>
73 73
74 74 ## branch link is only valid if it is a branch
75 75 <span class="spantag"><a href="${h.url('summary_home', repo_name=c.pull_request.org_repo.repo_name, anchor=c.org_ref_name)}">${c.org_ref_type}: ${c.org_ref_name}</a></span>
76 76 </div>
77 77 </div>
78 78 </div>
79 79 <div class="field">
80 80 <div class="label-summary">
81 81 <label>${_('Pull changes')}:</label>
82 82 </div>
83 83 <div class="input">
84 84 <div>
85 85 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
86 86 %if h.is_hg(c.pull_request.org_repo):
87 87 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
88 88 %elif h.is_git(c.pull_request.org_repo):
89 89 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
90 90 %endif
91 91 </div>
92 92 </div>
93 93 </div>
94 94 <div class="field">
95 95 <div class="label-summary">
96 96 <label>${_('Description')}:</label>
97 97 </div>
98 98 <div class="input">
99 99 <div style="white-space:pre-wrap">${h.urlify_commit(c.pull_request.description, c.repo_name)}</div>
100 100 </div>
101 101 </div>
102 102 <div class="field">
103 103 <div class="label-summary">
104 104 <label>${_('Created on')}:</label>
105 105 </div>
106 106 <div class="input">
107 107 <div>${h.fmt_date(c.pull_request.created_on)}</div>
108 108 </div>
109 109 </div>
110 110 </div>
111 111 </div>
112 112 ## REVIEWERS
113 113 <div style="float:left; border-left:1px dashed #eee">
114 114 <div class="pr-details-title">${_('Pull request reviewers')}</div>
115 115 <div id="reviewers" style="padding:0px 0px 5px 10px">
116 116 ## members goes here !
117 117 <div>
118 118 <ul id="review_members" class="group_members">
119 119 %for member,status in c.pull_request_reviewers:
120 120 <li id="reviewer_${member.user_id}">
121 121 <div class="reviewers_member">
122 122 <div class="reviewer_status tooltip" title="${h.tooltip(h.changeset_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
123 123 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
124 124 </div>
125 125 <div class="reviewer_gravatar gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
126 126 <div style="float:left;">${member.full_name} (${_('owner') if c.pull_request.user_id == member.user_id else _('reviewer')})</div>
127 127 <input type="hidden" value="${member.user_id}" name="review_members" />
128 128 %if not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.user_id == c.authuser.user_id):
129 129 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
130 130 <i class="icon-remove-sign" style="color: #FF4444;"></i>
131 131 </div>
132 132 %endif
133 133 </div>
134 134 </li>
135 135 %endfor
136 136 </ul>
137 137 </div>
138 138 %if not c.pull_request.is_closed():
139 139 <div class='ac'>
140 140 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.author.user_id == c.authuser.user_id:
141 141 <div class="reviewer_ac">
142 142 ${h.text('user', class_='yui-ac-input')}
143 143 <span class="help-block">${_('Add or remove reviewer to this pull request.')}</span>
144 144 <div id="reviewers_container"></div>
145 145 </div>
146 146 <div style="padding:0px 10px">
147 147 <span id="update_pull_request" class="btn btn-mini">${_('Save Changes')}</span>
148 148 </div>
149 149 %endif
150 150 </div>
151 151 %endif
152 152 </div>
153 153 </div>
154 154
155 155 <div style="overflow: auto; clear: both">
156 156 ##DIFF
157 157 <div class="table" style="float:left;clear:none">
158 <div id="body" class="diffblock">
159 <div style="white-space:pre-wrap;padding:5px">${_('Compare view')}</div>
158 <div class="diffblock">
159 <div style="padding:5px">
160 ${_('Compare view')}
161 </div>
160 162 </div>
161 163 <div id="changeset_compare_view_content">
162 164 ##CS
163 165 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
164 166 <%include file="/compare/compare_cs.html" />
165 167
166 168 ## FILES
167 169 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
168 170
169 171 % if c.limited_diff:
170 172 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
171 173 % else:
172 174 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
173 175 %endif
174 176
175 177 </div>
176 178 <div class="cs_files">
177 179 %if not c.files:
178 180 <span class="empty_data">${_('No files')}</span>
179 181 %endif
180 182 %for fid, change, f, stat in c.files:
181 183 <div class="cs_${change}">
182 184 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
183 185 <div class="changes">${h.fancy_file_stats(stat)}</div>
184 186 </div>
185 187 %endfor
186 188 </div>
187 189 % if c.limited_diff:
188 190 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
189 191 % endif
190 192 </div>
191 193 </div>
192 194 </div>
193 195 <script>
194 196 var _USERS_AC_DATA = ${c.users_array|n};
195 197 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
196 198 // TODO: switch this to pyroutes
197 199 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
198 200 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
199 201
200 202 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
201 203 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
202 204 pyroutes.register('pullrequest_update', "${url('pullrequest_update',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
203 205
204 206 </script>
205 207
206 208 ## diff block
207 209 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
208 210 %for fid, change, f, stat in c.files:
209 211 ${diff_block.diff_block_simple([c.changes[fid]])}
210 212 %endfor
211 213 % if c.limited_diff:
212 214 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h4>
213 215 % endif
214 216
215 217
216 218 ## template for inline comment form
217 219 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
218 220 ${comment.comment_inline_form()}
219 221
220 222 ## render comments and inlines
221 223 ${comment.generate_comments(include_pr=True)}
222 224
223 225 % if not c.pull_request.is_closed():
224 226 ## main comment form and it status
225 227 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
226 228 pull_request_id=c.pull_request.pull_request_id),
227 229 c.current_changeset_status,
228 230 is_pr=True, change_status=c.allowed_to_change_status)}
229 231 %endif
230 232
231 233 <script type="text/javascript">
232 234 YUE.onDOMReady(function(){
233 235 PullRequestAutoComplete('user', 'reviewers_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
234 236
235 237 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
236 238 var show = 'none';
237 239 var target = e.currentTarget;
238 240 if(target.checked){
239 241 var show = ''
240 242 }
241 243 var boxid = YUD.getAttribute(target,'id_for');
242 244 var comments = YUQ('#{0} .inline-comments'.format(boxid));
243 245 for(c in comments){
244 246 YUD.setStyle(comments[c],'display',show);
245 247 }
246 248 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
247 249 for(c in btns){
248 250 YUD.setStyle(btns[c],'display',show);
249 251 }
250 252 })
251 253
252 254 YUE.on(YUQ('.line'),'click',function(e){
253 255 var tr = e.currentTarget;
254 256 injectInlineForm(tr);
255 257 });
256 258
257 259 // inject comments into they proper positions
258 260 var file_comments = YUQ('.inline-comment-placeholder');
259 261 renderInlineComments(file_comments);
260 262
261 263 YUE.on(YUD.get('update_pull_request'),'click',function(e){
262 264 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
263 265 })
264 266 })
265 267 </script>
266 268
267 269 </div>
268 270
269 271 </%def>
General Comments 0
You need to be logged in to leave comments. Login now