##// END OF EJS Templates
Enabled compare engine for tags...
marcink -
r3010:bf96fd19 beta
parent child Browse files
Show More
@@ -1,148 +1,153 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.compare
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 compare controller for pylons showoing differences between two
7 7 repos, branches, bookmarks or tips
8 8
9 9 :created_on: May 6, 2012
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28
29 29 from webob.exc import HTTPNotFound
30 30 from pylons import request, response, session, tmpl_context as c, url
31 31 from pylons.controllers.util import abort, redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 38 from rhodecode.lib import diffs
39 39
40 40 from rhodecode.model.db import Repository
41 41 from rhodecode.model.pull_request import PullRequestModel
42 42 from webob.exc import HTTPBadRequest
43 43 from rhodecode.lib.utils2 import str2bool
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class CompareController(BaseRepoController):
49 49
50 50 @LoginRequired()
51 51 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
52 52 'repository.admin')
53 53 def __before__(self):
54 54 super(CompareController, self).__before__()
55 55
56 56 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
57 57 partial=False):
58 58 """
59 59 Safe way to get changeset if error occur it redirects to changeset with
60 60 proper message. If partial is set then don't do redirect raise Exception
61 61 instead
62 62
63 63 :param rev: revision to fetch
64 64 :param repo: repo instance
65 65 """
66 66
67 67 try:
68 68 type_, rev = rev
69 69 return repo.scm_instance.get_changeset(rev)
70 70 except EmptyRepositoryError, e:
71 71 if not redirect_after:
72 72 return None
73 73 h.flash(h.literal(_('There are no changesets yet')),
74 74 category='warning')
75 75 redirect(url('summary_home', repo_name=repo.repo_name))
76 76
77 77 except RepositoryError, e:
78 78 log.error(traceback.format_exc())
79 79 h.flash(str(e), category='warning')
80 80 if not partial:
81 81 redirect(h.url('summary_home', repo_name=repo.repo_name))
82 82 raise HTTPBadRequest()
83 83
84 84 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
85 85
86 86 org_repo = c.rhodecode_db_repo.repo_name
87 87 org_ref = (org_ref_type, org_ref)
88 88 other_ref = (other_ref_type, other_ref)
89 89 other_repo = request.GET.get('repo', org_repo)
90 90 bundle_compare = str2bool(request.GET.get('bundle', True))
91 91
92 92 c.swap_url = h.url('compare_url', repo_name=other_repo,
93 93 org_ref_type=other_ref[0], org_ref=other_ref[1],
94 94 other_ref_type=org_ref[0], other_ref=org_ref[1],
95 95 repo=org_repo, as_form=request.GET.get('as_form'),
96 96 bundle=bundle_compare)
97 97
98 98 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
99 99 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
100 100
101 101 if c.org_repo is None or c.other_repo is None:
102 102 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
103 103 raise HTTPNotFound
104 104
105 if c.org_repo.scm_instance.alias != 'hg':
106 log.error('Review not available for GIT REPOS')
105 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
106 log.error('compare of two remote repos not available for GIT REPOS')
107 107 raise HTTPNotFound
108
109 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
110 log.error('compare of two different kind of remote repos not available')
111 raise HTTPNotFound
112
108 113 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
109 114 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
110 115 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
111 116
112 117 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
113 118 org_repo, org_ref, other_repo, other_ref
114 119 )
115 120
116 121 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
117 122 c.cs_ranges])
118 123 c.target_repo = c.repo_name
119 124 # defines that we need hidden inputs with changesets
120 125 c.as_form = request.GET.get('as_form', False)
121 126 if partial:
122 127 return render('compare/compare_cs.html')
123 128
124 129 if not bundle_compare and c.cs_ranges:
125 130 # case we want a simple diff without incoming changesets, just
126 131 # for review purposes. Make the diff on the forked repo, with
127 132 # revision that is common ancestor
128 133 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
129 134 other_repo = org_repo
130 135
131 136 c.org_ref = org_ref[1]
132 137 c.other_ref = other_ref[1]
133 138
134 139 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
135 140 discovery_data, bundle_compare=bundle_compare)
136 141 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
137 142 _parsed = diff_processor.prepare()
138 143
139 144 c.files = []
140 145 c.changes = {}
141 146
142 147 for f in _parsed:
143 148 fid = h.FID('', f['filename'])
144 149 c.files.append([fid, f['operation'], f['filename'], f['stats']])
145 150 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
146 151 c.changes[fid] = [f['operation'], f['filename'], diff]
147 152
148 153 return render('compare/compare_diff.html')
@@ -1,185 +1,185 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.forks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 forks controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import formencode
27 27 import traceback
28 28 from formencode import htmlfill
29 29
30 30 from pylons import tmpl_context as c, request, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode.lib.helpers as h
35 35
36 36 from rhodecode.lib.helpers import Page
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
38 38 NotAnonymous, HasRepoPermissionAny, HasPermissionAllDecorator,\
39 39 HasPermissionAnyDecorator
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.forms import RepoForkForm
44 44 from rhodecode.model.scm import ScmModel
45 45 from rhodecode.lib.utils2 import safe_int
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class ForksController(BaseRepoController):
51 51
52 52 @LoginRequired()
53 53 def __before__(self):
54 54 super(ForksController, self).__before__()
55 55
56 56 def __load_defaults(self):
57 57 c.repo_groups = RepoGroup.groups_choices(check_perms=True)
58 58 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59 59 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
60 60 c.landing_revs_choices = choices
61 61
62 62 def __load_data(self, repo_name=None):
63 63 """
64 64 Load defaults settings for edit, and update
65 65
66 66 :param repo_name:
67 67 """
68 68 self.__load_defaults()
69 69
70 70 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
71 71 repo = db_repo.scm_instance
72 72
73 73 if c.repo_info is None:
74 74 h.flash(_('%s repository is not mapped to db perhaps'
75 75 ' it was created or renamed from the filesystem'
76 76 ' please run the application again'
77 77 ' in order to rescan repositories') % repo_name,
78 78 category='error')
79 79
80 80 return redirect(url('repos'))
81 81
82 82 c.default_user_id = User.get_by_username('default').user_id
83 83 c.in_public_journal = UserFollowing.query()\
84 84 .filter(UserFollowing.user_id == c.default_user_id)\
85 85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
86 86
87 87 if c.repo_info.stats:
88 88 last_rev = c.repo_info.stats.stat_on_revision+1
89 89 else:
90 90 last_rev = 0
91 91 c.stats_revision = last_rev
92 92
93 93 c.repo_last_rev = repo.count() if repo.revisions else 0
94 94
95 95 if last_rev == 0 or c.repo_last_rev == 0:
96 96 c.stats_percentage = 0
97 97 else:
98 98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
99 99 c.repo_last_rev) * 100)
100 100
101 101 defaults = RepoModel()._get_defaults(repo_name)
102 # add prefix to fork
103 defaults['repo_name'] = 'fork-' + defaults['repo_name']
102 # add suffix to fork
103 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
104 104 return defaults
105 105
106 106 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
107 107 'repository.admin')
108 108 def forks(self, repo_name):
109 109 p = safe_int(request.params.get('page', 1), 1)
110 110 repo_id = c.rhodecode_db_repo.repo_id
111 111 d = []
112 112 for r in Repository.get_repo_forks(repo_id):
113 113 if not HasRepoPermissionAny(
114 114 'repository.read', 'repository.write', 'repository.admin'
115 115 )(r.repo_name, 'get forks check'):
116 116 continue
117 117 d.append(r)
118 118 c.forks_pager = Page(d, page=p, items_per_page=20)
119 119
120 120 c.forks_data = render('/forks/forks_data.html')
121 121
122 122 if request.environ.get('HTTP_X_PARTIAL_XHR'):
123 123 return c.forks_data
124 124
125 125 return render('/forks/forks.html')
126 126
127 127 @NotAnonymous()
128 128 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
129 129 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
130 130 'repository.admin')
131 131 def fork(self, repo_name):
132 132 c.repo_info = Repository.get_by_repo_name(repo_name)
133 133 if not c.repo_info:
134 134 h.flash(_('%s repository is not mapped to db perhaps'
135 135 ' it was created or renamed from the file system'
136 136 ' please run the application again'
137 137 ' in order to rescan repositories') % repo_name,
138 138 category='error')
139 139
140 140 return redirect(url('home'))
141 141
142 142 defaults = self.__load_data(repo_name)
143 143
144 144 return htmlfill.render(
145 145 render('forks/fork.html'),
146 146 defaults=defaults,
147 147 encoding="UTF-8",
148 148 force_defaults=False
149 149 )
150 150
151 151 @NotAnonymous()
152 152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 154 'repository.admin')
155 155 def fork_create(self, repo_name):
156 156 self.__load_defaults()
157 157 c.repo_info = Repository.get_by_repo_name(repo_name)
158 158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 159 repo_groups=c.repo_groups_choices,
160 160 landing_revs=c.landing_revs_choices)()
161 161 form_result = {}
162 162 try:
163 163 form_result = _form.to_python(dict(request.POST))
164 164
165 165 # create fork is done sometimes async on celery, db transaction
166 166 # management is handled there.
167 167 RepoModel().create_fork(form_result, self.rhodecode_user.user_id)
168 168 h.flash(_('forked %s repository as %s') \
169 169 % (repo_name, form_result['repo_name']),
170 170 category='success')
171 171 except formencode.Invalid, errors:
172 172 c.new_repo = errors.value['repo_name']
173 173
174 174 return htmlfill.render(
175 175 render('forks/fork.html'),
176 176 defaults=errors.value,
177 177 errors=errors.error_dict or {},
178 178 prefix_error=False,
179 179 encoding="UTF-8")
180 180 except Exception:
181 181 log.error(traceback.format_exc())
182 182 h.flash(_('An error occurred during repository forking %s') %
183 183 repo_name, category='error')
184 184
185 185 return redirect(url('home'))
@@ -1,767 +1,771 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31 import traceback
32 32
33 33 from itertools import tee, imap
34 34
35 35 from mercurial import patch
36 36 from mercurial.mdiff import diffopts
37 37 from mercurial.bundlerepo import bundlerepository
38 38
39 39 from pylons.i18n.translation import _
40 40
41 41 from rhodecode.lib.compat import BytesIO
42 42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
43 43 from rhodecode.lib.vcs.exceptions import VCSError
44 44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.helpers import escape
47 47 from rhodecode.lib.utils import make_ui
48 48 from rhodecode.lib.utils2 import safe_unicode
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def wrap_to_table(str_):
54 54 return '''<table class="code-difftable">
55 55 <tr class="line no-comment">
56 56 <td class="lineno new"></td>
57 57 <td class="code no-comment"><pre>%s</pre></td>
58 58 </tr>
59 59 </table>''' % str_
60 60
61 61
62 62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
63 63 ignore_whitespace=True, line_context=3,
64 64 enable_comments=False):
65 65 """
66 66 returns a wrapped diff into a table, checks for cut_off_limit and presents
67 67 proper message
68 68 """
69 69
70 70 if filenode_old is None:
71 71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
72 72
73 73 if filenode_old.is_binary or filenode_new.is_binary:
74 74 diff = wrap_to_table(_('binary file'))
75 75 stats = (0, 0)
76 76 size = 0
77 77
78 78 elif cut_off_limit != -1 and (cut_off_limit is None or
79 79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
80 80
81 81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
82 82 ignore_whitespace=ignore_whitespace,
83 83 context=line_context)
84 84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
85 85
86 86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 87 stats = diff_processor.stat()
88 88 size = len(diff or '')
89 89 else:
90 90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 91 'diff menu to display this diff'))
92 92 stats = (0, 0)
93 93 size = 0
94 94 if not diff:
95 95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
96 96 [filenode_new, filenode_old])
97 97 if submodules:
98 98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
99 99 else:
100 100 diff = wrap_to_table(_('No changes detected'))
101 101
102 102 cs1 = filenode_old.changeset.raw_id
103 103 cs2 = filenode_new.changeset.raw_id
104 104
105 105 return size, cs1, cs2, diff, stats
106 106
107 107
108 108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
109 109 """
110 110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
111 111
112 112 :param ignore_whitespace: ignore whitespaces in diff
113 113 """
114 114 # make sure we pass in default context
115 115 context = context or 3
116 116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
117 117 [filenode_new, filenode_old])
118 118 if submodules:
119 119 return ''
120 120
121 121 for filenode in (filenode_old, filenode_new):
122 122 if not isinstance(filenode, FileNode):
123 123 raise VCSError("Given object should be FileNode object, not %s"
124 124 % filenode.__class__)
125 125
126 126 repo = filenode_new.changeset.repository
127 127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
129 129
130 130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
131 131 ignore_whitespace, context)
132 132 return vcs_gitdiff
133 133
134 134 NEW_FILENODE = 1
135 135 DEL_FILENODE = 2
136 136 MOD_FILENODE = 3
137 137 RENAMED_FILENODE = 4
138 138 CHMOD_FILENODE = 5
139 139
140 140
141 141 class DiffLimitExceeded(Exception):
142 142 pass
143 143
144 144
145 145 class LimitedDiffContainer(object):
146 146
147 147 def __init__(self, diff_limit, cur_diff_size, diff):
148 148 self.diff = diff
149 149 self.diff_limit = diff_limit
150 150 self.cur_diff_size = cur_diff_size
151 151
152 152 def __iter__(self):
153 153 for l in self.diff:
154 154 yield l
155 155
156 156
157 157 class DiffProcessor(object):
158 158 """
159 159 Give it a unified or git diff and it returns a list of the files that were
160 160 mentioned in the diff together with a dict of meta information that
161 161 can be used to render it in a HTML template.
162 162 """
163 163 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
164 164 _newline_marker = '\\ No newline at end of file\n'
165 165 _git_header_re = re.compile(r"""
166 166 #^diff[ ]--git
167 167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
168 168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
169 169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
170 170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
171 171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
172 172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
173 173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
174 174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
175 175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
176 176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
177 177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
178 178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
179 179 """, re.VERBOSE | re.MULTILINE)
180 180 _hg_header_re = re.compile(r"""
181 181 #^diff[ ]--git
182 182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
183 183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
184 184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
185 185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
186 186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
187 187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
188 188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
189 189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
190 190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
191 191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
192 192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
193 193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
194 194 """, re.VERBOSE | re.MULTILINE)
195 195
196 196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
197 197 """
198 198 :param diff: a text in diff format
199 199 :param vcs: type of version controll hg or git
200 200 :param format: format of diff passed, `udiff` or `gitdiff`
201 201 :param diff_limit: define the size of diff that is considered "big"
202 202 based on that parameter cut off will be triggered, set to None
203 203 to show full diff
204 204 """
205 205 if not isinstance(diff, basestring):
206 206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
207 207
208 208 self._diff = diff
209 209 self._format = format
210 210 self.adds = 0
211 211 self.removes = 0
212 212 # calculate diff size
213 213 self.diff_size = len(diff)
214 214 self.diff_limit = diff_limit
215 215 self.cur_diff_size = 0
216 216 self.parsed = False
217 217 self.parsed_diff = []
218 218 self.vcs = vcs
219 219
220 220 if format == 'gitdiff':
221 221 self.differ = self._highlight_line_difflib
222 222 self._parser = self._parse_gitdiff
223 223 else:
224 224 self.differ = self._highlight_line_udiff
225 225 self._parser = self._parse_udiff
226 226
227 227 def _copy_iterator(self):
228 228 """
229 229 make a fresh copy of generator, we should not iterate thru
230 230 an original as it's needed for repeating operations on
231 231 this instance of DiffProcessor
232 232 """
233 233 self.__udiff, iterator_copy = tee(self.__udiff)
234 234 return iterator_copy
235 235
236 236 def _escaper(self, string):
237 237 """
238 238 Escaper for diff escapes special chars and checks the diff limit
239 239
240 240 :param string:
241 241 :type string:
242 242 """
243 243
244 244 self.cur_diff_size += len(string)
245 245
246 246 # escaper get's iterated on each .next() call and it checks if each
247 247 # parsed line doesn't exceed the diff limit
248 248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
249 249 raise DiffLimitExceeded('Diff Limit Exceeded')
250 250
251 251 return safe_unicode(string).replace('&', '&amp;')\
252 252 .replace('<', '&lt;')\
253 253 .replace('>', '&gt;')
254 254
255 255 def _line_counter(self, l):
256 256 """
257 257 Checks each line and bumps total adds/removes for this diff
258 258
259 259 :param l:
260 260 """
261 261 if l.startswith('+') and not l.startswith('+++'):
262 262 self.adds += 1
263 263 elif l.startswith('-') and not l.startswith('---'):
264 264 self.removes += 1
265 265 return safe_unicode(l)
266 266
267 267 def _highlight_line_difflib(self, line, next_):
268 268 """
269 269 Highlight inline changes in both lines.
270 270 """
271 271
272 272 if line['action'] == 'del':
273 273 old, new = line, next_
274 274 else:
275 275 old, new = next_, line
276 276
277 277 oldwords = re.split(r'(\W)', old['line'])
278 278 newwords = re.split(r'(\W)', new['line'])
279 279
280 280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
281 281
282 282 oldfragments, newfragments = [], []
283 283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
284 284 oldfrag = ''.join(oldwords[i1:i2])
285 285 newfrag = ''.join(newwords[j1:j2])
286 286 if tag != 'equal':
287 287 if oldfrag:
288 288 oldfrag = '<del>%s</del>' % oldfrag
289 289 if newfrag:
290 290 newfrag = '<ins>%s</ins>' % newfrag
291 291 oldfragments.append(oldfrag)
292 292 newfragments.append(newfrag)
293 293
294 294 old['line'] = "".join(oldfragments)
295 295 new['line'] = "".join(newfragments)
296 296
297 297 def _highlight_line_udiff(self, line, next_):
298 298 """
299 299 Highlight inline changes in both lines.
300 300 """
301 301 start = 0
302 302 limit = min(len(line['line']), len(next_['line']))
303 303 while start < limit and line['line'][start] == next_['line'][start]:
304 304 start += 1
305 305 end = -1
306 306 limit -= start
307 307 while -end <= limit and line['line'][end] == next_['line'][end]:
308 308 end -= 1
309 309 end += 1
310 310 if start or end:
311 311 def do(l):
312 312 last = end + len(l['line'])
313 313 if l['action'] == 'add':
314 314 tag = 'ins'
315 315 else:
316 316 tag = 'del'
317 317 l['line'] = '%s<%s>%s</%s>%s' % (
318 318 l['line'][:start],
319 319 tag,
320 320 l['line'][start:last],
321 321 tag,
322 322 l['line'][last:]
323 323 )
324 324 do(line)
325 325 do(next_)
326 326
327 327 def _get_header(self, diff_chunk):
328 328 """
329 329 parses the diff header, and returns parts, and leftover diff
330 330 parts consists of 14 elements::
331 331
332 332 a_path, b_path, similarity_index, rename_from, rename_to,
333 333 old_mode, new_mode, new_file_mode, deleted_file_mode,
334 334 a_blob_id, b_blob_id, b_mode, a_file, b_file
335 335
336 336 :param diff_chunk:
337 337 :type diff_chunk:
338 338 """
339 339
340 340 if self.vcs == 'git':
341 341 match = self._git_header_re.match(diff_chunk)
342 342 diff = diff_chunk[match.end():]
343 343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
344 344 elif self.vcs == 'hg':
345 345 match = self._hg_header_re.match(diff_chunk)
346 346 diff = diff_chunk[match.end():]
347 347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
348 348 else:
349 349 raise Exception('VCS type %s is not supported' % self.vcs)
350 350
351 351 def _parse_gitdiff(self, inline_diff=True):
352 352 _files = []
353 353 diff_container = lambda arg: arg
354 354
355 355 ##split the diff in chunks of separate --git a/file b/file chunks
356 356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 357 binary = False
358 358 binary_msg = 'unknown binary'
359 359 head, diff = self._get_header(raw_diff)
360 360
361 361 if not head['a_file'] and head['b_file']:
362 362 op = 'A'
363 363 elif head['a_file'] and head['b_file']:
364 364 op = 'M'
365 365 elif head['a_file'] and not head['b_file']:
366 366 op = 'D'
367 367 else:
368 368 #probably we're dealing with a binary file 1
369 369 binary = True
370 370 if head['deleted_file_mode']:
371 371 op = 'D'
372 372 stats = ['b', DEL_FILENODE]
373 373 binary_msg = 'deleted binary file'
374 374 elif head['new_file_mode']:
375 375 op = 'A'
376 376 stats = ['b', NEW_FILENODE]
377 377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 378 else:
379 379 if head['new_mode'] and head['old_mode']:
380 380 stats = ['b', CHMOD_FILENODE]
381 381 op = 'M'
382 382 binary_msg = ('modified binary file chmod %s => %s'
383 383 % (head['old_mode'], head['new_mode']))
384 384 elif (head['rename_from'] and head['rename_to']
385 385 and head['rename_from'] != head['rename_to']):
386 386 stats = ['b', RENAMED_FILENODE]
387 387 op = 'M'
388 388 binary_msg = ('file renamed from %s to %s'
389 389 % (head['rename_from'], head['rename_to']))
390 390 else:
391 391 stats = ['b', MOD_FILENODE]
392 392 op = 'M'
393 393 binary_msg = 'modified binary file'
394 394
395 395 if not binary:
396 396 try:
397 397 chunks, stats = self._parse_lines(diff)
398 398 except DiffLimitExceeded:
399 399 diff_container = lambda _diff: LimitedDiffContainer(
400 400 self.diff_limit,
401 401 self.cur_diff_size,
402 402 _diff)
403 403 break
404 404 else:
405 405 chunks = []
406 406 chunks.append([{
407 407 'old_lineno': '',
408 408 'new_lineno': '',
409 409 'action': 'binary',
410 410 'line': binary_msg,
411 411 }])
412 412
413 413 _files.append({
414 414 'filename': head['b_path'],
415 415 'old_revision': head['a_blob_id'],
416 416 'new_revision': head['b_blob_id'],
417 417 'chunks': chunks,
418 418 'operation': op,
419 419 'stats': stats,
420 420 })
421 421
422 422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423 423
424 424 if inline_diff is False:
425 425 return diff_container(sorted(_files, key=sorter))
426 426
427 427 # highlight inline changes
428 428 for diff_data in _files:
429 429 for chunk in diff_data['chunks']:
430 430 lineiter = iter(chunk)
431 431 try:
432 432 while 1:
433 433 line = lineiter.next()
434 434 if line['action'] not in ['unmod', 'context']:
435 435 nextline = lineiter.next()
436 436 if nextline['action'] in ['unmod', 'context'] or \
437 437 nextline['action'] == line['action']:
438 438 continue
439 439 self.differ(line, nextline)
440 440 except StopIteration:
441 441 pass
442 442
443 443 return diff_container(sorted(_files, key=sorter))
444 444
445 445 def _parse_udiff(self, inline_diff=True):
446 446 raise NotImplementedError()
447 447
448 448 def _parse_lines(self, diff):
449 449 """
450 450 Parse the diff an return data for the template.
451 451 """
452 452
453 453 lineiter = iter(diff)
454 454 stats = [0, 0]
455 455
456 456 try:
457 457 chunks = []
458 458 line = lineiter.next()
459 459
460 460 while line:
461 461 lines = []
462 462 chunks.append(lines)
463 463
464 464 match = self._chunk_re.match(line)
465 465
466 466 if not match:
467 467 break
468 468
469 469 gr = match.groups()
470 470 (old_line, old_end,
471 471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 472 old_line -= 1
473 473 new_line -= 1
474 474
475 475 context = len(gr) == 5
476 476 old_end += old_line
477 477 new_end += new_line
478 478
479 479 if context:
480 480 # skip context only if it's first line
481 481 if int(gr[0]) > 1:
482 482 lines.append({
483 483 'old_lineno': '...',
484 484 'new_lineno': '...',
485 485 'action': 'context',
486 486 'line': line,
487 487 })
488 488
489 489 line = lineiter.next()
490 490
491 491 while old_line < old_end or new_line < new_end:
492 492 if line:
493 493 command = line[0]
494 494 if command in ['+', '-', ' ']:
495 495 #only modify the line if it's actually a diff
496 496 # thing
497 497 line = line[1:]
498 498 else:
499 499 command = ' '
500 500
501 501 affects_old = affects_new = False
502 502
503 503 # ignore those if we don't expect them
504 504 if command in '#@':
505 505 continue
506 506 elif command == '+':
507 507 affects_new = True
508 508 action = 'add'
509 509 stats[0] += 1
510 510 elif command == '-':
511 511 affects_old = True
512 512 action = 'del'
513 513 stats[1] += 1
514 514 else:
515 515 affects_old = affects_new = True
516 516 action = 'unmod'
517 517
518 518 if line != self._newline_marker:
519 519 old_line += affects_old
520 520 new_line += affects_new
521 521 lines.append({
522 522 'old_lineno': affects_old and old_line or '',
523 523 'new_lineno': affects_new and new_line or '',
524 524 'action': action,
525 525 'line': line
526 526 })
527 527
528 528 line = lineiter.next()
529 529
530 530 if line == self._newline_marker:
531 531 # we need to append to lines, since this is not
532 532 # counted in the line specs of diff
533 533 lines.append({
534 534 'old_lineno': '...',
535 535 'new_lineno': '...',
536 536 'action': 'context',
537 537 'line': line
538 538 })
539 539
540 540 except StopIteration:
541 541 pass
542 542 return chunks, stats
543 543
544 544 def _safe_id(self, idstring):
545 545 """Make a string safe for including in an id attribute.
546 546
547 547 The HTML spec says that id attributes 'must begin with
548 548 a letter ([A-Za-z]) and may be followed by any number
549 549 of letters, digits ([0-9]), hyphens ("-"), underscores
550 550 ("_"), colons (":"), and periods (".")'. These regexps
551 551 are slightly over-zealous, in that they remove colons
552 552 and periods unnecessarily.
553 553
554 554 Whitespace is transformed into underscores, and then
555 555 anything which is not a hyphen or a character that
556 556 matches \w (alphanumerics and underscore) is removed.
557 557
558 558 """
559 559 # Transform all whitespace to underscore
560 560 idstring = re.sub(r'\s', "_", '%s' % idstring)
561 561 # Remove everything that is not a hyphen or a member of \w
562 562 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
563 563 return idstring
564 564
565 565 def prepare(self, inline_diff=True):
566 566 """
567 567 Prepare the passed udiff for HTML rendering. It'l return a list
568 568 of dicts with diff information
569 569 """
570 570 parsed = self._parser(inline_diff=inline_diff)
571 571 self.parsed = True
572 572 self.parsed_diff = parsed
573 573 return parsed
574 574
575 575 def as_raw(self, diff_lines=None):
576 576 """
577 577 Returns raw string diff
578 578 """
579 579 return self._diff
580 580 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
581 581
582 582 def as_html(self, table_class='code-difftable', line_class='line',
583 583 new_lineno_class='lineno old', old_lineno_class='lineno new',
584 584 code_class='code', enable_comments=False, parsed_lines=None):
585 585 """
586 586 Return given diff as html table with customized css classes
587 587 """
588 588 def _link_to_if(condition, label, url):
589 589 """
590 590 Generates a link if condition is meet or just the label if not.
591 591 """
592 592
593 593 if condition:
594 594 return '''<a href="%(url)s">%(label)s</a>''' % {
595 595 'url': url,
596 596 'label': label
597 597 }
598 598 else:
599 599 return label
600 600 if not self.parsed:
601 601 self.prepare()
602 602
603 603 diff_lines = self.parsed_diff
604 604 if parsed_lines:
605 605 diff_lines = parsed_lines
606 606
607 607 _html_empty = True
608 608 _html = []
609 609 _html.append('''<table class="%(table_class)s">\n''' % {
610 610 'table_class': table_class
611 611 })
612 612
613 613 for diff in diff_lines:
614 614 for line in diff['chunks']:
615 615 _html_empty = False
616 616 for change in line:
617 617 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
618 618 'lc': line_class,
619 619 'action': change['action']
620 620 })
621 621 anchor_old_id = ''
622 622 anchor_new_id = ''
623 623 anchor_old = "%(filename)s_o%(oldline_no)s" % {
624 624 'filename': self._safe_id(diff['filename']),
625 625 'oldline_no': change['old_lineno']
626 626 }
627 627 anchor_new = "%(filename)s_n%(oldline_no)s" % {
628 628 'filename': self._safe_id(diff['filename']),
629 629 'oldline_no': change['new_lineno']
630 630 }
631 631 cond_old = (change['old_lineno'] != '...' and
632 632 change['old_lineno'])
633 633 cond_new = (change['new_lineno'] != '...' and
634 634 change['new_lineno'])
635 635 if cond_old:
636 636 anchor_old_id = 'id="%s"' % anchor_old
637 637 if cond_new:
638 638 anchor_new_id = 'id="%s"' % anchor_new
639 639 ###########################################################
640 640 # OLD LINE NUMBER
641 641 ###########################################################
642 642 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
643 643 'a_id': anchor_old_id,
644 644 'olc': old_lineno_class
645 645 })
646 646
647 647 _html.append('''%(link)s''' % {
648 648 'link': _link_to_if(True, change['old_lineno'],
649 649 '#%s' % anchor_old)
650 650 })
651 651 _html.append('''</td>\n''')
652 652 ###########################################################
653 653 # NEW LINE NUMBER
654 654 ###########################################################
655 655
656 656 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
657 657 'a_id': anchor_new_id,
658 658 'nlc': new_lineno_class
659 659 })
660 660
661 661 _html.append('''%(link)s''' % {
662 662 'link': _link_to_if(True, change['new_lineno'],
663 663 '#%s' % anchor_new)
664 664 })
665 665 _html.append('''</td>\n''')
666 666 ###########################################################
667 667 # CODE
668 668 ###########################################################
669 669 comments = '' if enable_comments else 'no-comment'
670 670 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
671 671 'cc': code_class,
672 672 'inc': comments
673 673 })
674 674 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
675 675 'code': change['line']
676 676 })
677 677
678 678 _html.append('''\t</td>''')
679 679 _html.append('''\n</tr>\n''')
680 680 _html.append('''</table>''')
681 681 if _html_empty:
682 682 return None
683 683 return ''.join(_html)
684 684
685 685 def stat(self):
686 686 """
687 687 Returns tuple of added, and removed lines for this instance
688 688 """
689 689 return self.adds, self.removes
690 690
691 691
692 692 class InMemoryBundleRepo(bundlerepository):
693 693 def __init__(self, ui, path, bundlestream):
694 694 self._tempparent = None
695 695 localrepo.localrepository.__init__(self, ui, path)
696 696 self.ui.setconfig('phases', 'publish', False)
697 697
698 698 self.bundle = bundlestream
699 699
700 700 # dict with the mapping 'filename' -> position in the bundle
701 701 self.bundlefilespos = {}
702 702
703 703
704 704 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
705 705 bundle_compare=False, context=3, ignore_whitespace=False):
706 706 """
707 707 General differ between branches, bookmarks, revisions of two remote related
708 708 repositories
709 709
710 710 :param org_repo:
711 711 :type org_repo:
712 712 :param org_ref:
713 713 :type org_ref:
714 714 :param other_repo:
715 715 :type other_repo:
716 716 :param other_ref:
717 717 :type other_ref:
718 718 """
719 719
720 720 bundlerepo = None
721 721 ignore_whitespace = ignore_whitespace
722 722 context = context
723 org_repo = org_repo.scm_instance._repo
723 org_repo_scm = org_repo.scm_instance
724 org_repo = org_repo_scm._repo
724 725 other_repo = other_repo.scm_instance._repo
725 726 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
726 727 org_ref = org_ref[1]
727 728 other_ref = other_ref[1]
728 729
729 if org_repo != other_repo and bundle_compare:
730 if org_repo == other_repo:
731 log.debug('running diff between %s@%s and %s@%s'
732 % (org_repo, org_ref, other_repo, other_ref))
733 _diff = org_repo_scm.get_diff(rev1=other_ref, rev2=org_ref,
734 ignore_whitespace=ignore_whitespace, context=context)
735 return _diff
736
737 elif bundle_compare:
730 738
731 739 common, incoming, rheads = discovery_data
732 740 other_repo_peer = localrepo.locallegacypeer(other_repo.local())
733 741 # create a bundle (uncompressed if other repo is not local)
734 742 if other_repo_peer.capable('getbundle') and incoming:
735 743 # disable repo hooks here since it's just bundle !
736 744 # patch and reset hooks section of UI config to not run any
737 745 # hooks on fetching archives with subrepos
738 746 for k, _ in other_repo.ui.configitems('hooks'):
739 747 other_repo.ui.setconfig('hooks', k, None)
740 748
741 749 unbundle = other_repo.getbundle('incoming', common=common,
742 750 heads=None)
743 751
744 752 buf = BytesIO()
745 753 while True:
746 754 chunk = unbundle._stream.read(1024 * 4)
747 755 if not chunk:
748 756 break
749 757 buf.write(chunk)
750 758
751 759 buf.seek(0)
752 760 # replace chunked _stream with data that can do tell() and seek()
753 761 unbundle._stream = buf
754 762
755 763 ui = make_ui('db')
756 764 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
757 765 bundlestream=unbundle)
758 766
759 767 return ''.join(patch.diff(bundlerepo or org_repo,
760 768 node1=org_repo[org_ref].node(),
761 769 node2=other_repo[other_ref].node(),
762 770 opts=opts))
763 else:
764 log.debug('running diff between %s@%s and %s@%s'
765 % (org_repo, org_ref, other_repo, other_ref))
766 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
767 opts=opts))
771
@@ -1,262 +1,277 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.pull_request
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull request model for RhodeCode
7 7
8 8 :created_on: Jun 6, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import binascii
28 28 import datetime
29 import re
29 30
30 31 from pylons.i18n.translation import _
31 32
32 33 from rhodecode.model.meta import Session
33 34 from rhodecode.lib import helpers as h
34 35 from rhodecode.model import BaseModel
35 36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
36 37 from rhodecode.model.notification import NotificationModel
37 38 from rhodecode.lib.utils2 import safe_unicode
38 39
39 40 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
40 41 findcommonoutgoing
41 42
42 43 log = logging.getLogger(__name__)
43 44
44 45
45 46 class PullRequestModel(BaseModel):
46 47
47 48 cls = PullRequest
48 49
49 50 def __get_pull_request(self, pull_request):
50 51 return self._get_instance(PullRequest, pull_request)
51 52
52 53 def get_all(self, repo):
53 54 repo = self._get_repo(repo)
54 55 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
55 56
56 57 def create(self, created_by, org_repo, org_ref, other_repo,
57 58 other_ref, revisions, reviewers, title, description=None):
58 59
59 60 created_by_user = self._get_user(created_by)
60 61 org_repo = self._get_repo(org_repo)
61 62 other_repo = self._get_repo(other_repo)
62 63
63 64 new = PullRequest()
64 65 new.org_repo = org_repo
65 66 new.org_ref = org_ref
66 67 new.other_repo = other_repo
67 68 new.other_ref = other_ref
68 69 new.revisions = revisions
69 70 new.title = title
70 71 new.description = description
71 72 new.author = created_by_user
72 73 self.sa.add(new)
73 74 Session().flush()
74 75 #members
75 76 for member in reviewers:
76 77 _usr = self._get_user(member)
77 78 reviewer = PullRequestReviewers(_usr, new)
78 79 self.sa.add(reviewer)
79 80
80 81 #notification to reviewers
81 82 notif = NotificationModel()
82 83
83 84 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
84 85 pull_request_id=new.pull_request_id,
85 86 qualified=True,
86 87 )
87 88 subject = safe_unicode(
88 89 h.link_to(
89 90 _('%(user)s wants you to review pull request #%(pr_id)s') % \
90 91 {'user': created_by_user.username,
91 92 'pr_id': new.pull_request_id},
92 93 pr_url
93 94 )
94 95 )
95 96 body = description
96 97 kwargs = {
97 98 'pr_title': title,
98 99 'pr_user_created': h.person(created_by_user.email),
99 100 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
100 101 qualified=True,),
101 102 'pr_url': pr_url,
102 103 'pr_revisions': revisions
103 104 }
104 105 notif.create(created_by=created_by_user, subject=subject, body=body,
105 106 recipients=reviewers,
106 107 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
107 108 return new
108 109
109 110 def update_reviewers(self, pull_request, reviewers_ids):
110 111 reviewers_ids = set(reviewers_ids)
111 112 pull_request = self.__get_pull_request(pull_request)
112 113 current_reviewers = PullRequestReviewers.query()\
113 114 .filter(PullRequestReviewers.pull_request==
114 115 pull_request)\
115 116 .all()
116 117 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
117 118
118 119 to_add = reviewers_ids.difference(current_reviewers_ids)
119 120 to_remove = current_reviewers_ids.difference(reviewers_ids)
120 121
121 122 log.debug("Adding %s reviewers" % to_add)
122 123 log.debug("Removing %s reviewers" % to_remove)
123 124
124 125 for uid in to_add:
125 126 _usr = self._get_user(uid)
126 127 reviewer = PullRequestReviewers(_usr, pull_request)
127 128 self.sa.add(reviewer)
128 129
129 130 for uid in to_remove:
130 131 reviewer = PullRequestReviewers.query()\
131 132 .filter(PullRequestReviewers.user_id==uid,
132 133 PullRequestReviewers.pull_request==pull_request)\
133 134 .scalar()
134 135 if reviewer:
135 136 self.sa.delete(reviewer)
136 137
137 138 def delete(self, pull_request):
138 139 pull_request = self.__get_pull_request(pull_request)
139 140 Session().delete(pull_request)
140 141
141 142 def close_pull_request(self, pull_request):
142 143 pull_request = self.__get_pull_request(pull_request)
143 144 pull_request.status = PullRequest.STATUS_CLOSED
144 145 pull_request.updated_on = datetime.datetime.now()
145 146 self.sa.add(pull_request)
146 147
147 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref,
148 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref,
148 149 discovery_data):
149 150 """
150 151 Returns a list of changesets that are incoming from org_repo@org_ref
151 152 to other_repo@other_ref
152 153
153 154 :param org_repo:
154 155 :type org_repo:
155 156 :param org_ref:
156 157 :type org_ref:
157 158 :param other_repo:
158 159 :type other_repo:
159 160 :param other_ref:
160 161 :type other_ref:
161 162 :param tmp:
162 163 :type tmp:
163 164 """
164 165 changesets = []
165 166 #case two independent repos
166 167 common, incoming, rheads = discovery_data
167 168 if org_repo != other_repo and incoming:
168 169 obj = findcommonoutgoing(org_repo._repo,
169 170 localrepo.locallegacypeer(other_repo._repo.local()),
170 171 force=True)
171 172 revs = obj.missing
172 173
173 174 for cs in reversed(map(binascii.hexlify, revs)):
174 175 changesets.append(org_repo.get_changeset(cs))
175 176 else:
177 #no remote compare do it on the same repository
178 if alias == 'hg':
176 179 _revset_predicates = {
177 180 'branch': 'branch',
178 181 'book': 'bookmark',
179 182 'tag': 'tag',
180 183 'rev': 'id',
181 184 }
182 185
183 186 revs = [
184 187 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
185 188 _revset_predicates[org_ref[0]], org_ref[1],
186 189 _revset_predicates[other_ref[0]], other_ref[1]
187 190 )
188 191 ]
189 192
190 193 out = scmutil.revrange(org_repo._repo, revs)
191 194 for cs in reversed(out):
192 195 changesets.append(org_repo.get_changeset(cs))
196 elif alias == 'git':
197 so, se = org_repo.run_git_command(
198 'log --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
199 other_ref[1])
200 )
201 ids = re.findall(r'[0-9a-fA-F]{40}', so)
202 for cs in reversed(ids):
203 changesets.append(org_repo.get_changeset(cs))
193 204
194 205 return changesets
195 206
196 207 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
197 208 """
198 209 Get's mercurial discovery data used to calculate difference between
199 210 repos and refs
200 211
201 212 :param org_repo:
202 213 :type org_repo:
203 214 :param org_ref:
204 215 :type org_ref:
205 216 :param other_repo:
206 217 :type other_repo:
207 218 :param other_ref:
208 219 :type other_ref:
209 220 """
210 221
211 222 _org_repo = org_repo._repo
212 223 org_rev_type, org_rev = org_ref
213 224
214 225 _other_repo = other_repo._repo
215 226 other_rev_type, other_rev = other_ref
216 227
217 228 log.debug('Doing discovery for %s@%s vs %s@%s' % (
218 229 org_repo, org_ref, other_repo, other_ref)
219 230 )
220 231
221 232 #log.debug('Filter heads are %s[%s]' % ('', org_ref[1]))
222 233 org_peer = localrepo.locallegacypeer(_org_repo.local())
223 234 tmp = discovery.findcommonincoming(
224 235 repo=_other_repo, # other_repo we check for incoming
225 236 remote=org_peer, # org_repo source for incoming
226 237 heads=[_other_repo[other_rev].node(),
227 238 _org_repo[org_rev].node()],
228 239 force=True
229 240 )
230 241 return tmp
231 242
232 243 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
233 244 """
234 Returns a tuple of incomming changesets, and discoverydata cache
245 Returns a tuple of incomming changesets, and discoverydata cache for
246 mercurial repositories
235 247
236 248 :param org_repo:
237 249 :type org_repo:
238 250 :param org_ref:
239 251 :type org_ref:
240 252 :param other_repo:
241 253 :type other_repo:
242 254 :param other_ref:
243 255 :type other_ref:
244 256 """
245 257
246 258 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
247 259 raise Exception('org_ref must be a two element list/tuple')
248 260
249 261 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
250 262 raise Exception('other_ref must be a two element list/tuple')
251 263
252 discovery_data = self._get_discovery(org_repo.scm_instance,
253 org_ref,
254 other_repo.scm_instance,
255 other_ref)
256 cs_ranges = self._get_changesets(org_repo.scm_instance,
257 org_ref,
258 other_repo.scm_instance,
259 other_ref,
264 org_repo_scm = org_repo.scm_instance
265 other_repo_scm = other_repo.scm_instance
266
267 alias = org_repo.scm_instance.alias
268 discovery_data = [None, None, None]
269 if alias == 'hg':
270 discovery_data = self._get_discovery(org_repo_scm, org_ref,
271 other_repo_scm, other_ref)
272 cs_ranges = self._get_changesets(alias,
273 org_repo_scm, org_ref,
274 other_repo_scm, other_ref,
260 275 discovery_data)
261 276
262 277 return cs_ranges, discovery_data
@@ -1,94 +1,93 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Branches') % c.repo_name} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 <input class="q_filter_box" id="q_filter_branches" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
10 10 ${h.link_to(_(u'Home'),h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('branches')}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('branches')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <!-- end box / title -->
28 28 %if c.repo_branches:
29 29 <div class="info_box" id="compare_branches" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare branches')}</a></div>
30 30 %endif
31 31 <div class="table">
32 32 <%include file='branches_data.html'/>
33 33 </div>
34 34 </div>
35 35 <script type="text/javascript">
36 36 YUE.on('compare_branches','click',function(e){
37 37 YUE.preventDefault(e);
38 38 var org = YUQ('input[name=compare_org]:checked')[0];
39 39 var other = YUQ('input[name=compare_other]:checked')[0];
40 40
41 41 if(org && other){
42 42 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='__ORG__',other_ref_type='branch',other_ref='__OTHER__')}";
43 43 var u = compare_url.replace('__ORG__',org.value)
44 44 .replace('__OTHER__',other.value);
45 45 window.location=u;
46 46 }
47
48 })
47 });
49 48 // main table sorting
50 49 var myColumnDefs = [
51 50 {key:"name",label:"${_('Name')}",sortable:true},
52 51 {key:"date",label:"${_('Date')}",sortable:true,
53 52 sortOptions: { sortFunction: dateSort }},
54 53 {key:"author",label:"${_('Author')}",sortable:true},
55 54 {key:"revision",label:"${_('Revision')}",sortable:true,
56 55 sortOptions: { sortFunction: revisionSort }},
57 56 {key:"compare",label:"${_('Compare')}",sortable:false,},
58 57 ];
59 58
60 59 var myDataSource = new YAHOO.util.DataSource(YUD.get("branches_data"));
61 60
62 61 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
63 62
64 63 myDataSource.responseSchema = {
65 64 fields: [
66 65 {key:"name"},
67 66 {key:"date"},
68 67 {key:"author"},
69 68 {key:"revision"},
70 69 {key:"compare"},
71 70 ]
72 71 };
73 72
74 73 var myDataTable = new YAHOO.widget.DataTable("table_wrap", myColumnDefs, myDataSource,
75 74 {
76 75 sortedBy:{key:"name",dir:"asc"},
77 76 MSG_SORTASC:"${_('Click to sort ascending')}",
78 77 MSG_SORTDESC:"${_('Click to sort descending')}",
79 78 MSG_EMPTY:"${_('No records found.')}",
80 79 MSG_ERROR:"${_('Data error.')}",
81 80 MSG_LOADING:"${_('Loading...')}",
82 81 }
83 82 );
84 83 myDataTable.subscribe('postRenderEvent',function(oArgs) {
85 84 tooltip_activate();
86 85 var func = function(node){
87 86 return node.parentNode.parentNode.parentNode.parentNode.parentNode;
88 87 }
89 88 q_filter('q_filter_branches',YUQ('div.table tr td .logtags .branchtag a'),func);
90 89 });
91 90
92 91 </script>
93 92
94 93 </%def>
@@ -1,58 +1,58 b''
1 1 %if c.repo_branches:
2 2 <div id="table_wrap" class="yui-skin-sam">
3 3 <table id="branches_data">
4 4 <thead>
5 5 <tr>
6 <th class="left">${_('name')}</th>
7 <th class="left">${_('date')}</th>
8 <th class="left">${_('author')}</th>
9 <th class="left">${_('revision')}</th>
10 <th class="left">${_('compare')}</th>
6 <th class="left">${_('Name')}</th>
7 <th class="left">${_('Date')}</th>
8 <th class="left">${_('Author')}</th>
9 <th class="left">${_('Revision')}</th>
10 <th class="left">${_('Compare')}</th>
11 11 </tr>
12 12 </thead>
13 13 %for cnt,branch in enumerate(c.repo_branches.items()):
14 14 <tr class="parity${cnt%2}">
15 15 <td>
16 16 <span class="logtags">
17 17 <span class="branchtag">${h.link_to(branch[0],
18 18 h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id))}</span>
19 19 </span>
20 20 </td>
21 21 <td><span class="tooltip" title="${h.tooltip(h.age(branch[1].date))}">${h.fmt_date(branch[1].date)}</span></td>
22 22 <td title="${branch[1].author}">${h.person(branch[1].author)}</td>
23 23 <td>
24 24 <div>
25 25 <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id)}">r${branch[1].revision}:${h.short_id(branch[1].raw_id)}</a></pre>
26 26 </div>
27 27 </td>
28 28 <td>
29 29 <input class="branch-compare" type="radio" name="compare_org" value="${branch[0]}"/>
30 30 <input class="branch-compare" type="radio" name="compare_other" value="${branch[0]}"/>
31 31 </td>
32 32 </tr>
33 33 %endfor
34 34 % if hasattr(c,'repo_closed_branches') and c.repo_closed_branches:
35 35 %for cnt,branch in enumerate(c.repo_closed_branches.items()):
36 36 <tr class="parity${cnt%2}">
37 37 <td>
38 38 <span class="logtags">
39 39 <span class="branchtag">${h.link_to(branch[0]+' [closed]',
40 40 h.url('changeset_home',repo_name=c.repo_name,revision=branch[1].raw_id))}</span>
41 41 </span>
42 42 </td>
43 43 <td><span class="tooltip" title="${h.tooltip(h.age(branch[1].date))}">${h.fmt_date(branch[1].date)}</span></td>
44 44 <td title="${branch[1].author}">${h.person(branch[1].author)}</td>
45 45 <td>
46 46 <div>
47 47 <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=branch[1].raw_id)}">r${branch[1].revision}:${h.short_id(branch[1].raw_id)}</a></pre>
48 48 </div>
49 49 </td>
50 50 <td></td>
51 51 </tr>
52 52 %endfor
53 53 %endif
54 54 </table>
55 55 </div>
56 56 %else:
57 57 ${_('There are no branches yet')}
58 58 %endif
@@ -1,303 +1,303 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(_(u'Home'),h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 <% size = c.size if c.size <= c.total_cs else c.total_cs %>
15 15 ${_('Changelog')} - ${ungettext('showing %d out of %d revision', 'showing %d out of %d revisions', size) % (size, c.total_cs)}
16 16 </%def>
17 17
18 18 <%def name="page_nav()">
19 19 ${self.menu('changelog')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <!-- box / title -->
25 25 <div class="title">
26 26 ${self.breadcrumbs()}
27 27 </div>
28 28 <div class="table">
29 29 % if c.pagination:
30 30 <div id="graph">
31 31 <div id="graph_nodes">
32 32 <canvas id="graph_canvas"></canvas>
33 33 </div>
34 34 <div id="graph_content">
35 35 <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;">
36 36 <a href="#" class="ui-btn small" id="rev_range_container" style="display:none"></a>
37 37 <a href="#" class="ui-btn small" id="rev_range_clear" style="display:none">${_('Clear selection')}</a>
38 38
39 39 %if c.rhodecode_db_repo.fork:
40 <a title="${_('compare fork with %s' % c.rhodecode_db_repo.fork.repo_name)}" href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref=request.GET.get('branch') or 'default',other_ref_type='branch',other_ref='default',repo=c.rhodecode_db_repo.fork.repo_name)}" class="ui-btn small">${_('Compare fork')}</a>
40 <a title="${_('compare fork with %s' % c.rhodecode_db_repo.fork.repo_name)}" href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref=request.GET.get('branch') or 'default',other_ref_type='branch',other_ref='default',repo=c.rhodecode_db_repo.fork.repo_name)}" class="ui-btn small">${_('Compare fork with parent')}</a>
41 41 %endif
42 42 %if h.is_hg(c.rhodecode_repo):
43 43 <a id="open_new_pr" href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a>
44 44 %endif
45 45 </div>
46 46 <div class="container_header">
47 47 ${h.form(h.url.current(),method='get')}
48 48 <div class="info_box" style="float:left">
49 49 ${h.submit('set',_('Show'),class_="ui-btn")}
50 50 ${h.text('size',size=1,value=c.size)}
51 51 ${_('revisions')}
52 52 </div>
53 53 ${h.end_form()}
54 54 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
55 55 </div>
56 56
57 57 %for cnt,cs in enumerate(c.pagination):
58 58 <div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
59 59 <div class="left">
60 60 <div>
61 61 ${h.checkbox(cs.raw_id,class_="changeset_range")}
62 62 <span class="tooltip" title="${h.tooltip(h.age(cs.date))}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
63 63 </div>
64 64 <div class="author">
65 65 <div class="gravatar">
66 66 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),16)}"/>
67 67 </div>
68 68 <div title="${cs.author}" class="user">${h.shorter(h.person(cs.author),22)}</div>
69 69 </div>
70 70 <div class="date">${h.fmt_date(cs.date)}</div>
71 71 </div>
72 72 <div class="mid">
73 73 <div class="message">${h.urlify_commit(cs.message, c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
74 74 <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
75 75 </div>
76 76 <div class="right">
77 77 <div class="changes">
78 78 <div id="changed_total_${cs.raw_id}" style="float:right;" class="changed_total tooltip" title="${h.tooltip(_('Affected number of files, click to show more details'))}">${len(cs.affected_files)}</div>
79 79 <div class="comments-container">
80 80 %if len(c.comments.get(cs.raw_id,[])) > 0:
81 81 <div class="comments-cnt" title="${('comments')}">
82 82 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
83 83 <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
84 84 <img src="${h.url('/images/icons/comments.png')}">
85 85 </a>
86 86 </div>
87 87 %endif
88 88 </div>
89 89 <div class="changeset-status-container">
90 90 %if c.statuses.get(cs.raw_id):
91 91 <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
92 92 <div class="changeset-status-ico">
93 93 %if c.statuses.get(cs.raw_id)[2]:
94 94 <a class="tooltip" title="${_('Click to open associated pull request')}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></a>
95 95 %else:
96 96 <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" />
97 97 %endif
98 98 </div>
99 99 %endif
100 100 </div>
101 101 </div>
102 102 %if cs.parents:
103 103 %for p_cs in reversed(cs.parents):
104 104 <div class="parent">${_('Parent')}
105 105 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
106 106 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
107 107 </div>
108 108 %endfor
109 109 %else:
110 110 <div class="parent">${_('No parents')}</div>
111 111 %endif
112 112
113 113 <span class="logtags">
114 114 %if len(cs.parents)>1:
115 115 <span class="merge">${_('merge')}</span>
116 116 %endif
117 117 %if cs.branch:
118 118 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
119 119 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
120 120 </span>
121 121 %endif
122 122 %if h.is_hg(c.rhodecode_repo):
123 123 %for book in cs.bookmarks:
124 124 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
125 125 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
126 126 </span>
127 127 %endfor
128 128 %endif
129 129 %for tag in cs.tags:
130 130 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
131 131 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
132 132 %endfor
133 133 </span>
134 134 </div>
135 135 </div>
136 136
137 137 %endfor
138 138 <div class="pagination-wh pagination-left">
139 139 ${c.pagination.pager('$link_previous ~2~ $link_next')}
140 140 </div>
141 141 </div>
142 142 </div>
143 143
144 144 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
145 145 <script type="text/javascript">
146 146 YAHOO.util.Event.onDOMReady(function(){
147 147
148 148 //Monitor range checkboxes and build a link to changesets
149 149 //ranges
150 150 var checkboxes = YUD.getElementsByClassName('changeset_range');
151 151 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
152 152 YUE.on(checkboxes,'click',function(e){
153 153 var clicked_cb = e.currentTarget;
154 154 var checked_checkboxes = [];
155 155 for (pos in checkboxes){
156 156 if(checkboxes[pos].checked){
157 157 checked_checkboxes.push(checkboxes[pos]);
158 158 }
159 159 }
160 160 if(YUD.get('open_new_pr')){
161 161 if(checked_checkboxes.length>0){
162 162 // modify open pull request to show we have selected cs
163 163 YUD.get('open_new_pr').innerHTML = _TM['Open new pull request for selected changesets'];
164 164
165 165 }else{
166 166 YUD.get('open_new_pr').innerHTML = _TM['Open new pull request'];
167 167 }
168 168 }
169 169
170 170 if(checked_checkboxes.length>1){
171 171 var rev_end = checked_checkboxes[0].name;
172 172 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
173 173
174 174 // now select all checkboxes in the middle.
175 175 var checked = false;
176 176 for (var i=0; i<checkboxes.length; i++){
177 177 var cb = checkboxes[i];
178 178 var rev = cb.name;
179 179
180 180 if (rev == rev_end){
181 181 checked = true;
182 182 }
183 183 if (checked){
184 184 cb.checked = true;
185 185 }
186 186 else{
187 187 cb.checked = false;
188 188 }
189 189 if (rev == rev_start){
190 190 checked = false;
191 191 }
192 192
193 193 }
194 194
195 195 var url = url_tmpl.replace('__REVRANGE__',
196 196 rev_start+'...'+rev_end);
197 197
198 198 var link = _TM['Show selected changes __S -> __E'];
199 199 link = link.replace('__S',rev_start.substr(0,6));
200 200 link = link.replace('__E',rev_end.substr(0,6));
201 201 YUD.get('rev_range_container').href = url;
202 202 YUD.get('rev_range_container').innerHTML = link;
203 203 YUD.setStyle('rev_range_container','display','');
204 204 YUD.setStyle('rev_range_clear','display','');
205 205
206 206 }
207 207 else{
208 208 YUD.setStyle('rev_range_container','display','none');
209 209 YUD.setStyle('rev_range_clear','display','none');
210 210 }
211 211 });
212 212 YUE.on('rev_range_clear','click',function(e){
213 213 for (var i=0; i<checkboxes.length; i++){
214 214 var cb = checkboxes[i];
215 215 cb.checked = false;
216 216 }
217 217 YUE.preventDefault(e);
218 218 })
219 219 var msgs = YUQ('.message');
220 220 // get first element height
221 221 var el = YUQ('#graph_content .container')[0];
222 222 var row_h = el.clientHeight;
223 223 for(var i=0;i<msgs.length;i++){
224 224 var m = msgs[i];
225 225
226 226 var h = m.clientHeight;
227 227 var pad = YUD.getStyle(m,'padding');
228 228 if(h > row_h){
229 229 var offset = row_h - (h+12);
230 230 YUD.setStyle(m.nextElementSibling,'display','block');
231 231 YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
232 232 };
233 233 }
234 234 YUE.on(YUQ('.expand'),'click',function(e){
235 235 var elem = e.currentTarget.parentNode.parentNode;
236 236 YUD.setStyle(e.currentTarget,'display','none');
237 237 YUD.setStyle(elem,'height','auto');
238 238
239 239 //redraw the graph, line_count and jsdata are global vars
240 240 set_canvas(100);
241 241
242 242 var r = new BranchRenderer();
243 243 r.render(jsdata,100,line_count);
244 244
245 245 })
246 246
247 247 // Fetch changeset details
248 248 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
249 249 var id = e.currentTarget.id;
250 250 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}";
251 251 var url = url.replace('__CS__',id.replace('changed_total_',''));
252 252 ypjax(url,id,function(){tooltip_activate()});
253 253 });
254 254
255 255 // change branch filter
256 256 YUE.on(YUD.get('branch_filter'),'change',function(e){
257 257 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
258 258 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
259 259 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
260 260 var url = url.replace('__BRANCH__',selected_branch);
261 261 if(selected_branch != ''){
262 262 window.location = url;
263 263 }else{
264 264 window.location = url_main;
265 265 }
266 266
267 267 });
268 268
269 269 function set_canvas(width) {
270 270 var c = document.getElementById('graph_nodes');
271 271 var t = document.getElementById('graph_content');
272 272 canvas = document.getElementById('graph_canvas');
273 273 var div_h = t.clientHeight;
274 274 c.style.height=div_h+'px';
275 275 canvas.setAttribute('height',div_h);
276 276 c.style.height=width+'px';
277 277 canvas.setAttribute('width',width);
278 278 };
279 279 var heads = 1;
280 280 var line_count = 0;
281 281 var jsdata = ${c.jsdata|n};
282 282
283 283 for (var i=0;i<jsdata.length;i++) {
284 284 var in_l = jsdata[i][2];
285 285 for (var j in in_l) {
286 286 var m = in_l[j][1];
287 287 if (m > line_count)
288 288 line_count = m;
289 289 }
290 290 }
291 291 set_canvas(100);
292 292
293 293 var r = new BranchRenderer();
294 294 r.render(jsdata,100,line_count);
295 295
296 296 });
297 297 </script>
298 298 %else:
299 299 ${_('There are no changes yet')}
300 300 %endif
301 301 </div>
302 302 </div>
303 303 </%def>
@@ -1,76 +1,93 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Tags') % c.repo_name} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 <input class="q_filter_box" id="q_filter_tags" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
11 11 ${h.link_to(_(u'Home'),h.url('/'))}
12 12 &raquo;
13 13 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
14 14 &raquo;
15 15 ${_('tags')}
16 16 </%def>
17 17
18 18 <%def name="page_nav()">
19 19 ${self.menu('tags')}
20 20 </%def>
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <!-- end box / title -->
28 %if c.repo_tags:
29 <div class="info_box" id="compare_tags" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare tags')}</a></div>
30 %endif
28 31 <div class="table">
29 32 <%include file='tags_data.html'/>
30 33 </div>
31 34 </div>
32 35 <script type="text/javascript">
36 YUE.on('compare_tags','click',function(e){
37 YUE.preventDefault(e);
38 var org = YUQ('input[name=compare_org]:checked')[0];
39 var other = YUQ('input[name=compare_other]:checked')[0];
40
41 if(org && other){
42 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='tag',org_ref='__ORG__',other_ref_type='tag',other_ref='__OTHER__')}";
43 var u = compare_url.replace('__ORG__',org.value)
44 .replace('__OTHER__',other.value);
45 window.location=u;
46 }
47 });
33 48
34 49 // main table sorting
35 50 var myColumnDefs = [
36 51 {key:"name",label:"${_('Name')}",sortable:true},
37 52 {key:"date",label:"${_('Date')}",sortable:true,
38 53 sortOptions: { sortFunction: dateSort }},
39 54 {key:"author",label:"${_('Author')}",sortable:true},
40 55 {key:"revision",label:"${_('Revision')}",sortable:true,
41 56 sortOptions: { sortFunction: revisionSort }},
57 {key:"compare",label:"${_('Compare')}",sortable:false,},
42 58 ];
43 59
44 60 var myDataSource = new YAHOO.util.DataSource(YUD.get("tags_data"));
45 61
46 62 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
47 63
48 64 myDataSource.responseSchema = {
49 65 fields: [
50 66 {key:"name"},
51 67 {key:"date"},
52 68 {key:"author"},
53 69 {key:"revision"},
70 {key:"compare"},
54 71 ]
55 72 };
56 73
57 74 var myDataTable = new YAHOO.widget.DataTable("table_wrap", myColumnDefs, myDataSource,
58 75 {
59 76 sortedBy:{key:"name",dir:"asc"},
60 77 MSG_SORTASC:"${_('Click to sort ascending')}",
61 78 MSG_SORTDESC:"${_('Click to sort descending')}",
62 79 MSG_EMPTY:"${_('No records found.')}",
63 80 MSG_ERROR:"${_('Data error.')}",
64 81 MSG_LOADING:"${_('Loading...')}",
65 82 }
66 83 );
67 84 myDataTable.subscribe('postRenderEvent',function(oArgs) {
68 85 tooltip_activate();
69 86 var func = function(node){
70 87 return node.parentNode.parentNode.parentNode.parentNode.parentNode;
71 88 }
72 89 q_filter('q_filter_tags',YUQ('div.table tr td .logtags .tagtag a'),func);
73 90 });
74 91
75 92 </script>
76 93 </%def>
@@ -1,34 +1,39 b''
1 1 %if c.repo_tags:
2 2 <div id="table_wrap" class="yui-skin-sam">
3 3 <table id="tags_data">
4 4 <thead>
5 5 <tr>
6 6 <th class="left">${_('Name')}</th>
7 7 <th class="left">${_('Date')}</th>
8 8 <th class="left">${_('Author')}</th>
9 9 <th class="left">${_('Revision')}</th>
10 <th class="left">${_('Compare')}</th>
10 11 </tr>
11 12 </thead>
12 13 %for cnt,tag in enumerate(c.repo_tags.items()):
13 14 <tr class="parity${cnt%2}">
14 15 <td>
15 16 <span class="logtags">
16 17 <span class="tagtag">${h.link_to(tag[0],
17 18 h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id))}
18 19 </span>
19 20 </span>
20 21 </td>
21 22 <td><span class="tooltip" title="${h.tooltip(h.age(tag[1].date))}">${h.fmt_date(tag[1].date)}</span></td>
22 23 <td title="${tag[1].author}">${h.person(tag[1].author)}</td>
23 24 <td>
24 25 <div>
25 26 <pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=tag[1].raw_id)}">r${tag[1].revision}:${h.short_id(tag[1].raw_id)}</a></pre>
26 27 </div>
27 28 </td>
29 <td>
30 <input class="branch-compare" type="radio" name="compare_org" value="${tag[0]}"/>
31 <input class="branch-compare" type="radio" name="compare_other" value="${tag[0]}"/>
32 </td>
28 33 </tr>
29 34 %endfor
30 35 </table>
31 36 </div>
32 37 %else:
33 38 ${_('There are no tags yet')}
34 39 %endif
General Comments 0
You need to be logged in to leave comments. Login now