##// END OF EJS Templates
diffs: drop diffs.differ...
Mads Kiilerich -
r3718:b2575bdb beta
parent child Browse files
Show More
@@ -1,188 +1,191 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.compare
3 rhodecode.controllers.compare
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 compare controller for pylons showing differences between two
6 compare controller for pylons showing differences between two
7 repos, branches, bookmarks or tips
7 repos, branches, bookmarks or tips
8
8
9 :created_on: May 6, 2012
9 :created_on: May 6, 2012
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from webob.exc import HTTPNotFound
29 from webob.exc import HTTPNotFound
30 from pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, session, tmpl_context as c, url
31 from pylons.controllers.util import abort, redirect
31 from pylons.controllers.util import abort, redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from rhodecode.lib import diffs
38 from rhodecode.lib import diffs
39
39
40 from rhodecode.model.db import Repository
40 from rhodecode.model.db import Repository
41 from rhodecode.model.pull_request import PullRequestModel
41 from rhodecode.model.pull_request import PullRequestModel
42 from webob.exc import HTTPBadRequest
42 from webob.exc import HTTPBadRequest
43 from rhodecode.lib.diffs import LimitedDiffContainer
43 from rhodecode.lib.diffs import LimitedDiffContainer
44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CompareController(BaseRepoController):
49 class CompareController(BaseRepoController):
50
50
51 @LoginRequired()
51 @LoginRequired()
52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 'repository.admin')
53 'repository.admin')
54 def __before__(self):
54 def __before__(self):
55 super(CompareController, self).__before__()
55 super(CompareController, self).__before__()
56
56
57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
58 partial=False):
58 partial=False):
59 """
59 """
60 Safe way to get changeset if error occur it redirects to changeset with
60 Safe way to get changeset if error occur it redirects to changeset with
61 proper message. If partial is set then don't do redirect raise Exception
61 proper message. If partial is set then don't do redirect raise Exception
62 instead
62 instead
63
63
64 :param rev: revision to fetch
64 :param rev: revision to fetch
65 :param repo: repo instance
65 :param repo: repo instance
66 """
66 """
67
67
68 try:
68 try:
69 type_, rev = rev
69 type_, rev = rev
70 return repo.scm_instance.get_changeset(rev)
70 return repo.scm_instance.get_changeset(rev)
71 except EmptyRepositoryError, e:
71 except EmptyRepositoryError, e:
72 if not redirect_after:
72 if not redirect_after:
73 return None
73 return None
74 h.flash(h.literal(_('There are no changesets yet')),
74 h.flash(h.literal(_('There are no changesets yet')),
75 category='warning')
75 category='warning')
76 redirect(url('summary_home', repo_name=repo.repo_name))
76 redirect(url('summary_home', repo_name=repo.repo_name))
77
77
78 except RepositoryError, e:
78 except RepositoryError, e:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 h.flash(str(e), category='warning')
80 h.flash(str(e), category='warning')
81 if not partial:
81 if not partial:
82 redirect(h.url('summary_home', repo_name=repo.repo_name))
82 redirect(h.url('summary_home', repo_name=repo.repo_name))
83 raise HTTPBadRequest()
83 raise HTTPBadRequest()
84
84
85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
86 # org_ref will be evaluated in org_repo
86 # org_ref will be evaluated in org_repo
87 org_repo = c.rhodecode_db_repo.repo_name
87 org_repo = c.rhodecode_db_repo.repo_name
88 org_ref = (org_ref_type, org_ref)
88 org_ref = (org_ref_type, org_ref)
89 # other_ref will be evaluated in other_repo
89 # other_ref will be evaluated in other_repo
90 other_ref = (other_ref_type, other_ref)
90 other_ref = (other_ref_type, other_ref)
91 other_repo = request.GET.get('other_repo', org_repo)
91 other_repo = request.GET.get('other_repo', org_repo)
92 # If merge is True:
92 # If merge is True:
93 # Show what org would get if merged with other:
93 # Show what org would get if merged with other:
94 # List changesets that are ancestors of other but not of org.
94 # List changesets that are ancestors of other but not of org.
95 # New changesets in org is thus ignored.
95 # New changesets in org is thus ignored.
96 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
96 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
97 # If merge is False:
97 # If merge is False:
98 # Make a raw diff from org to other, no matter if related or not.
98 # Make a raw diff from org to other, no matter if related or not.
99 # Changesets in one and not in the other will be ignored
99 # Changesets in one and not in the other will be ignored
100 merge = bool(request.GET.get('merge'))
100 merge = bool(request.GET.get('merge'))
101 # fulldiff disables cut_off_limit
101 # fulldiff disables cut_off_limit
102 c.fulldiff = request.GET.get('fulldiff')
102 c.fulldiff = request.GET.get('fulldiff')
103 # partial uses compare_cs.html template directly
103 # partial uses compare_cs.html template directly
104 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
104 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
105 # as_form puts hidden input field with changeset revisions
105 # as_form puts hidden input field with changeset revisions
106 c.as_form = partial and request.GET.get('as_form')
106 c.as_form = partial and request.GET.get('as_form')
107 # swap url for compare_diff page - never partial and never as_form
107 # swap url for compare_diff page - never partial and never as_form
108 c.swap_url = h.url('compare_url',
108 c.swap_url = h.url('compare_url',
109 repo_name=other_repo,
109 repo_name=other_repo,
110 org_ref_type=other_ref[0], org_ref=other_ref[1],
110 org_ref_type=other_ref[0], org_ref=other_ref[1],
111 other_repo=org_repo,
111 other_repo=org_repo,
112 other_ref_type=org_ref[0], other_ref=org_ref[1],
112 other_ref_type=org_ref[0], other_ref=org_ref[1],
113 merge=merge or '')
113 merge=merge or '')
114
114
115 org_repo = Repository.get_by_repo_name(org_repo)
115 org_repo = Repository.get_by_repo_name(org_repo)
116 other_repo = Repository.get_by_repo_name(other_repo)
116 other_repo = Repository.get_by_repo_name(other_repo)
117
117
118 if org_repo is None:
118 if org_repo is None:
119 log.error('Could not find org repo %s' % org_repo)
119 log.error('Could not find org repo %s' % org_repo)
120 raise HTTPNotFound
120 raise HTTPNotFound
121 if other_repo is None:
121 if other_repo is None:
122 log.error('Could not find other repo %s' % other_repo)
122 log.error('Could not find other repo %s' % other_repo)
123 raise HTTPNotFound
123 raise HTTPNotFound
124
124
125 if org_repo != other_repo and h.is_git(org_repo):
125 if org_repo != other_repo and h.is_git(org_repo):
126 log.error('compare of two remote repos not available for GIT REPOS')
126 log.error('compare of two remote repos not available for GIT REPOS')
127 raise HTTPNotFound
127 raise HTTPNotFound
128
128
129 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
129 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
130 log.error('compare of two different kind of remote repos not available')
130 log.error('compare of two different kind of remote repos not available')
131 raise HTTPNotFound
131 raise HTTPNotFound
132
132
133 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
133 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
134 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
134 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
135
135
136 c.org_repo = org_repo
136 c.org_repo = org_repo
137 c.other_repo = other_repo
137 c.other_repo = other_repo
138 c.org_ref = org_ref[1]
138 c.org_ref = org_ref[1]
139 c.other_ref = other_ref[1]
139 c.other_ref = other_ref[1]
140 c.org_ref_type = org_ref[0]
140 c.org_ref_type = org_ref[0]
141 c.other_ref_type = other_ref[0]
141 c.other_ref_type = other_ref[0]
142
142
143 c.cs_ranges, c.ancestor = PullRequestModel().get_compare_data(
143 c.cs_ranges, c.ancestor = PullRequestModel().get_compare_data(
144 org_repo, org_ref, other_repo, other_ref, merge)
144 org_repo, org_ref, other_repo, other_ref, merge)
145
145
146 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
146 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
147 c.cs_ranges])
147 c.cs_ranges])
148 if partial:
148 if partial:
149 assert c.ancestor
149 assert c.ancestor
150 return render('compare/compare_cs.html')
150 return render('compare/compare_cs.html')
151
151
152 if c.ancestor:
152 if c.ancestor:
153 assert merge
153 assert merge
154 # case we want a simple diff without incoming changesets,
154 # case we want a simple diff without incoming changesets,
155 # previewing what will be merged.
155 # previewing what will be merged.
156 # Make the diff on the other repo (which is known to have other_ref)
156 # Make the diff on the other repo (which is known to have other_ref)
157 log.debug('Using ancestor %s as org_ref instead of %s'
157 log.debug('Using ancestor %s as org_ref instead of %s'
158 % (c.ancestor, org_ref))
158 % (c.ancestor, org_ref))
159 org_ref = ('rev', c.ancestor)
159 org_ref = ('rev', c.ancestor)
160 org_repo = other_repo
160 org_repo = other_repo
161
161
162 diff_limit = self.cut_off_limit if not c.fulldiff else None
162 diff_limit = self.cut_off_limit if not c.fulldiff else None
163
163
164 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref)
164 log.debug('running diff between %s@%s and %s@%s'
165 % (org_repo.scm_instance.path, org_ref,
166 other_repo.scm_instance.path, other_ref))
167 _diff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
165
168
166 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
169 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
167 diff_limit=diff_limit)
170 diff_limit=diff_limit)
168 _parsed = diff_processor.prepare()
171 _parsed = diff_processor.prepare()
169
172
170 c.limited_diff = False
173 c.limited_diff = False
171 if isinstance(_parsed, LimitedDiffContainer):
174 if isinstance(_parsed, LimitedDiffContainer):
172 c.limited_diff = True
175 c.limited_diff = True
173
176
174 c.files = []
177 c.files = []
175 c.changes = {}
178 c.changes = {}
176 c.lines_added = 0
179 c.lines_added = 0
177 c.lines_deleted = 0
180 c.lines_deleted = 0
178 for f in _parsed:
181 for f in _parsed:
179 st = f['stats']
182 st = f['stats']
180 if st[0] != 'b':
183 if st[0] != 'b':
181 c.lines_added += st[0]
184 c.lines_added += st[0]
182 c.lines_deleted += st[1]
185 c.lines_deleted += st[1]
183 fid = h.FID('', f['filename'])
186 fid = h.FID('', f['filename'])
184 c.files.append([fid, f['operation'], f['filename'], f['stats']])
187 c.files.append([fid, f['operation'], f['filename'], f['stats']])
185 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
188 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
186 c.changes[fid] = [f['operation'], f['filename'], diff]
189 c.changes[fid] = [f['operation'], f['filename'], diff]
187
190
188 return render('compare/compare_diff.html')
191 return render('compare/compare_diff.html')
@@ -1,513 +1,517 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import formencode
27 import formencode
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
30 from collections import defaultdict
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from pylons import request, response, session, tmpl_context as c, url
33 from pylons import request, response, session, tmpl_context as c, url
34 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.lib.compat import json
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.base import BaseRepoController, render
38 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 NotAnonymous
40 NotAnonymous
41 from rhodecode.lib.helpers import Page
41 from rhodecode.lib.helpers import Page
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import diffs
43 from rhodecode.lib import diffs
44 from rhodecode.lib.utils import action_logger, jsonify
44 from rhodecode.lib.utils import action_logger, jsonify
45 from rhodecode.lib.vcs.utils import safe_str
45 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
46 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
46 from rhodecode.lib.vcs.backends.base import EmptyChangeset
47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
47 from rhodecode.lib.diffs import LimitedDiffContainer
48 from rhodecode.lib.diffs import LimitedDiffContainer
48 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
49 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
49 ChangesetComment
50 ChangesetComment
50 from rhodecode.model.pull_request import PullRequestModel
51 from rhodecode.model.pull_request import PullRequestModel
51 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
52 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.repo import RepoModel
53 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.comment import ChangesetCommentsModel
54 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 from rhodecode.model.forms import PullRequestForm
56 from rhodecode.model.forms import PullRequestForm
56 from mercurial import scmutil
57 from mercurial import scmutil
57 from rhodecode.lib.utils2 import safe_int
58 from rhodecode.lib.utils2 import safe_int
58
59
59 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
60
61
61
62
62 class PullrequestsController(BaseRepoController):
63 class PullrequestsController(BaseRepoController):
63
64
64 @LoginRequired()
65 @LoginRequired()
65 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
66 'repository.admin')
67 'repository.admin')
67 def __before__(self):
68 def __before__(self):
68 super(PullrequestsController, self).__before__()
69 super(PullrequestsController, self).__before__()
69 repo_model = RepoModel()
70 repo_model = RepoModel()
70 c.users_array = repo_model.get_users_js()
71 c.users_array = repo_model.get_users_js()
71 c.users_groups_array = repo_model.get_users_groups_js()
72 c.users_groups_array = repo_model.get_users_groups_js()
72
73
73 def _get_repo_refs(self, repo, rev=None, branch_rev=None):
74 def _get_repo_refs(self, repo, rev=None, branch_rev=None):
74 """return a structure with repo's interesting changesets, suitable for
75 """return a structure with repo's interesting changesets, suitable for
75 the selectors in pullrequest.html"""
76 the selectors in pullrequest.html"""
76
77
77 # list named branches that has been merged to this named branch - it should probably merge back
78 # list named branches that has been merged to this named branch - it should probably merge back
78 peers = []
79 peers = []
79 if branch_rev:
80 if branch_rev:
80 # not restricting to merge() would also get branch point and be better
81 # not restricting to merge() would also get branch point and be better
81 # (especially because it would get the branch point) ... but is currently too expensive
82 # (especially because it would get the branch point) ... but is currently too expensive
82 revs = ["sort(parents(branch(id('%s')) and merge()) - branch(id('%s')))" %
83 revs = ["sort(parents(branch(id('%s')) and merge()) - branch(id('%s')))" %
83 (branch_rev, branch_rev)]
84 (branch_rev, branch_rev)]
84 otherbranches = {}
85 otherbranches = {}
85 for i in scmutil.revrange(repo._repo, revs):
86 for i in scmutil.revrange(repo._repo, revs):
86 cs = repo.get_changeset(i)
87 cs = repo.get_changeset(i)
87 otherbranches[cs.branch] = cs.raw_id
88 otherbranches[cs.branch] = cs.raw_id
88 for branch, node in otherbranches.iteritems():
89 for branch, node in otherbranches.iteritems():
89 selected = 'branch:%s:%s' % (branch, node)
90 selected = 'branch:%s:%s' % (branch, node)
90 peers.append((selected, branch))
91 peers.append((selected, branch))
91
92
92 selected = None
93 selected = None
93 branches = []
94 branches = []
94 for branch, branchrev in repo.branches.iteritems():
95 for branch, branchrev in repo.branches.iteritems():
95 n = 'branch:%s:%s' % (branch, branchrev)
96 n = 'branch:%s:%s' % (branch, branchrev)
96 branches.append((n, branch))
97 branches.append((n, branch))
97 if rev == branchrev:
98 if rev == branchrev:
98 selected = n
99 selected = n
99 bookmarks = []
100 bookmarks = []
100 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
101 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
101 n = 'book:%s:%s' % (bookmark, bookmarkrev)
102 n = 'book:%s:%s' % (bookmark, bookmarkrev)
102 bookmarks.append((n, bookmark))
103 bookmarks.append((n, bookmark))
103 if rev == bookmarkrev:
104 if rev == bookmarkrev:
104 selected = n
105 selected = n
105 tags = []
106 tags = []
106 for tag, tagrev in repo.tags.iteritems():
107 for tag, tagrev in repo.tags.iteritems():
107 n = 'tag:%s:%s' % (tag, tagrev)
108 n = 'tag:%s:%s' % (tag, tagrev)
108 tags.append((n, tag))
109 tags.append((n, tag))
109 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
110 if rev == tagrev and tag != 'tip': # tip is not a real tag - and its branch is better
110 selected = n
111 selected = n
111
112
112 # prio 1: rev was selected as existing entry above
113 # prio 1: rev was selected as existing entry above
113
114
114 # prio 2: create special entry for rev; rev _must_ be used
115 # prio 2: create special entry for rev; rev _must_ be used
115 specials = []
116 specials = []
116 if rev and selected is None:
117 if rev and selected is None:
117 selected = 'rev:%s:%s' % (rev, rev)
118 selected = 'rev:%s:%s' % (rev, rev)
118 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
119 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
119
120
120 # prio 3: most recent peer branch
121 # prio 3: most recent peer branch
121 if peers and not selected:
122 if peers and not selected:
122 selected = peers[0][0][0]
123 selected = peers[0][0][0]
123
124
124 # prio 4: tip revision
125 # prio 4: tip revision
125 if not selected:
126 if not selected:
126 selected = 'tag:tip:%s' % repo.tags['tip']
127 selected = 'tag:tip:%s' % repo.tags['tip']
127
128
128 groups = [(specials, _("Special")),
129 groups = [(specials, _("Special")),
129 (peers, _("Peer branches")),
130 (peers, _("Peer branches")),
130 (bookmarks, _("Bookmarks")),
131 (bookmarks, _("Bookmarks")),
131 (branches, _("Branches")),
132 (branches, _("Branches")),
132 (tags, _("Tags")),
133 (tags, _("Tags")),
133 ]
134 ]
134 return [g for g in groups if g[0]], selected
135 return [g for g in groups if g[0]], selected
135
136
136 def _get_is_allowed_change_status(self, pull_request):
137 def _get_is_allowed_change_status(self, pull_request):
137 owner = self.rhodecode_user.user_id == pull_request.user_id
138 owner = self.rhodecode_user.user_id == pull_request.user_id
138 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
139 reviewer = self.rhodecode_user.user_id in [x.user_id for x in
139 pull_request.reviewers]
140 pull_request.reviewers]
140 return (self.rhodecode_user.admin or owner or reviewer)
141 return (self.rhodecode_user.admin or owner or reviewer)
141
142
142 def show_all(self, repo_name):
143 def show_all(self, repo_name):
143 c.pull_requests = PullRequestModel().get_all(repo_name)
144 c.pull_requests = PullRequestModel().get_all(repo_name)
144 c.repo_name = repo_name
145 c.repo_name = repo_name
145 p = safe_int(request.params.get('page', 1), 1)
146 p = safe_int(request.params.get('page', 1), 1)
146
147
147 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
148 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
148
149
149 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
150 c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
150
151
151 if request.environ.get('HTTP_X_PARTIAL_XHR'):
152 if request.environ.get('HTTP_X_PARTIAL_XHR'):
152 return c.pullrequest_data
153 return c.pullrequest_data
153
154
154 return render('/pullrequests/pullrequest_show_all.html')
155 return render('/pullrequests/pullrequest_show_all.html')
155
156
156 @NotAnonymous()
157 @NotAnonymous()
157 def index(self):
158 def index(self):
158 org_repo = c.rhodecode_db_repo
159 org_repo = c.rhodecode_db_repo
159
160
160 if org_repo.scm_instance.alias != 'hg':
161 if org_repo.scm_instance.alias != 'hg':
161 log.error('Review not available for GIT REPOS')
162 log.error('Review not available for GIT REPOS')
162 raise HTTPNotFound
163 raise HTTPNotFound
163
164
164 try:
165 try:
165 org_repo.scm_instance.get_changeset()
166 org_repo.scm_instance.get_changeset()
166 except EmptyRepositoryError, e:
167 except EmptyRepositoryError, e:
167 h.flash(h.literal(_('There are no changesets yet')),
168 h.flash(h.literal(_('There are no changesets yet')),
168 category='warning')
169 category='warning')
169 redirect(url('summary_home', repo_name=org_repo.repo_name))
170 redirect(url('summary_home', repo_name=org_repo.repo_name))
170
171
171 org_rev = request.GET.get('rev_end')
172 org_rev = request.GET.get('rev_end')
172 # rev_start is not directly useful - its parent could however be used
173 # rev_start is not directly useful - its parent could however be used
173 # as default for other and thus give a simple compare view
174 # as default for other and thus give a simple compare view
174 #other_rev = request.POST.get('rev_start')
175 #other_rev = request.POST.get('rev_start')
175
176
176 c.org_repos = []
177 c.org_repos = []
177 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
178 c.org_repos.append((org_repo.repo_name, org_repo.repo_name))
178 c.default_org_repo = org_repo.repo_name
179 c.default_org_repo = org_repo.repo_name
179 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, org_rev)
180 c.org_refs, c.default_org_ref = self._get_repo_refs(org_repo.scm_instance, org_rev)
180
181
181 c.other_repos = []
182 c.other_repos = []
182 other_repos_info = {}
183 other_repos_info = {}
183
184
184 def add_other_repo(repo, branch_rev=None):
185 def add_other_repo(repo, branch_rev=None):
185 if repo.repo_name in other_repos_info: # shouldn't happen
186 if repo.repo_name in other_repos_info: # shouldn't happen
186 return
187 return
187 c.other_repos.append((repo.repo_name, repo.repo_name))
188 c.other_repos.append((repo.repo_name, repo.repo_name))
188 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
189 other_refs, selected_other_ref = self._get_repo_refs(repo.scm_instance, branch_rev=branch_rev)
189 other_repos_info[repo.repo_name] = {
190 other_repos_info[repo.repo_name] = {
190 'user': dict(user_id=repo.user.user_id,
191 'user': dict(user_id=repo.user.user_id,
191 username=repo.user.username,
192 username=repo.user.username,
192 firstname=repo.user.firstname,
193 firstname=repo.user.firstname,
193 lastname=repo.user.lastname,
194 lastname=repo.user.lastname,
194 gravatar_link=h.gravatar_url(repo.user.email, 14)),
195 gravatar_link=h.gravatar_url(repo.user.email, 14)),
195 'description': repo.description.split('\n', 1)[0],
196 'description': repo.description.split('\n', 1)[0],
196 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
197 'revs': h.select('other_ref', selected_other_ref, other_refs, class_='refs')
197 }
198 }
198
199
199 # add org repo to other so we can open pull request against peer branches on itself
200 # add org repo to other so we can open pull request against peer branches on itself
200 add_other_repo(org_repo, branch_rev=org_rev)
201 add_other_repo(org_repo, branch_rev=org_rev)
201 c.default_other_repo = org_repo.repo_name
202 c.default_other_repo = org_repo.repo_name
202
203
203 # gather forks and add to this list ... even though it is rare to
204 # gather forks and add to this list ... even though it is rare to
204 # request forks to pull from their parent
205 # request forks to pull from their parent
205 for fork in org_repo.forks:
206 for fork in org_repo.forks:
206 add_other_repo(fork)
207 add_other_repo(fork)
207
208
208 # add parents of this fork also, but only if it's not empty
209 # add parents of this fork also, but only if it's not empty
209 if org_repo.parent and org_repo.parent.scm_instance.revisions:
210 if org_repo.parent and org_repo.parent.scm_instance.revisions:
210 add_other_repo(org_repo.parent)
211 add_other_repo(org_repo.parent)
211 c.default_other_repo = org_repo.parent.repo_name
212 c.default_other_repo = org_repo.parent.repo_name
212
213
213 c.default_other_repo_info = other_repos_info[c.default_other_repo]
214 c.default_other_repo_info = other_repos_info[c.default_other_repo]
214 c.other_repos_info = json.dumps(other_repos_info)
215 c.other_repos_info = json.dumps(other_repos_info)
215
216
216 return render('/pullrequests/pullrequest.html')
217 return render('/pullrequests/pullrequest.html')
217
218
218 @NotAnonymous()
219 @NotAnonymous()
219 def create(self, repo_name):
220 def create(self, repo_name):
220 repo = RepoModel()._get_repo(repo_name)
221 repo = RepoModel()._get_repo(repo_name)
221 try:
222 try:
222 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
223 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
223 except formencode.Invalid, errors:
224 except formencode.Invalid, errors:
224 log.error(traceback.format_exc())
225 log.error(traceback.format_exc())
225 if errors.error_dict.get('revisions'):
226 if errors.error_dict.get('revisions'):
226 msg = 'Revisions: %s' % errors.error_dict['revisions']
227 msg = 'Revisions: %s' % errors.error_dict['revisions']
227 elif errors.error_dict.get('pullrequest_title'):
228 elif errors.error_dict.get('pullrequest_title'):
228 msg = _('Pull request requires a title with min. 3 chars')
229 msg = _('Pull request requires a title with min. 3 chars')
229 else:
230 else:
230 msg = _('Error creating pull request')
231 msg = _('Error creating pull request')
231
232
232 h.flash(msg, 'error')
233 h.flash(msg, 'error')
233 return redirect(url('pullrequest_home', repo_name=repo_name))
234 return redirect(url('pullrequest_home', repo_name=repo_name))
234
235
235 org_repo = _form['org_repo']
236 org_repo = _form['org_repo']
236 org_ref = 'rev:merge:%s' % _form['merge_rev']
237 org_ref = 'rev:merge:%s' % _form['merge_rev']
237 other_repo = _form['other_repo']
238 other_repo = _form['other_repo']
238 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev']
239 other_ref = 'rev:ancestor:%s' % _form['ancestor_rev']
239 revisions = _form['revisions']
240 revisions = _form['revisions']
240 reviewers = _form['review_members']
241 reviewers = _form['review_members']
241
242
242 title = _form['pullrequest_title']
243 title = _form['pullrequest_title']
243 description = _form['pullrequest_desc']
244 description = _form['pullrequest_desc']
244
245
245 try:
246 try:
246 pull_request = PullRequestModel().create(
247 pull_request = PullRequestModel().create(
247 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
248 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
248 other_ref, revisions, reviewers, title, description
249 other_ref, revisions, reviewers, title, description
249 )
250 )
250 Session().commit()
251 Session().commit()
251 h.flash(_('Successfully opened new pull request'),
252 h.flash(_('Successfully opened new pull request'),
252 category='success')
253 category='success')
253 except Exception:
254 except Exception:
254 h.flash(_('Error occurred during sending pull request'),
255 h.flash(_('Error occurred during sending pull request'),
255 category='error')
256 category='error')
256 log.error(traceback.format_exc())
257 log.error(traceback.format_exc())
257 return redirect(url('pullrequest_home', repo_name=repo_name))
258 return redirect(url('pullrequest_home', repo_name=repo_name))
258
259
259 return redirect(url('pullrequest_show', repo_name=other_repo,
260 return redirect(url('pullrequest_show', repo_name=other_repo,
260 pull_request_id=pull_request.pull_request_id))
261 pull_request_id=pull_request.pull_request_id))
261
262
262 @NotAnonymous()
263 @NotAnonymous()
263 @jsonify
264 @jsonify
264 def update(self, repo_name, pull_request_id):
265 def update(self, repo_name, pull_request_id):
265 pull_request = PullRequest.get_or_404(pull_request_id)
266 pull_request = PullRequest.get_or_404(pull_request_id)
266 if pull_request.is_closed():
267 if pull_request.is_closed():
267 raise HTTPForbidden()
268 raise HTTPForbidden()
268 #only owner or admin can update it
269 #only owner or admin can update it
269 owner = pull_request.author.user_id == c.rhodecode_user.user_id
270 owner = pull_request.author.user_id == c.rhodecode_user.user_id
270 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
271 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
271 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
272 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
272 request.POST.get('reviewers_ids', '').split(',')))
273 request.POST.get('reviewers_ids', '').split(',')))
273
274
274 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
275 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
275 Session().commit()
276 Session().commit()
276 return True
277 return True
277 raise HTTPForbidden()
278 raise HTTPForbidden()
278
279
279 @NotAnonymous()
280 @NotAnonymous()
280 @jsonify
281 @jsonify
281 def delete(self, repo_name, pull_request_id):
282 def delete(self, repo_name, pull_request_id):
282 pull_request = PullRequest.get_or_404(pull_request_id)
283 pull_request = PullRequest.get_or_404(pull_request_id)
283 #only owner can delete it !
284 #only owner can delete it !
284 if pull_request.author.user_id == c.rhodecode_user.user_id:
285 if pull_request.author.user_id == c.rhodecode_user.user_id:
285 PullRequestModel().delete(pull_request)
286 PullRequestModel().delete(pull_request)
286 Session().commit()
287 Session().commit()
287 h.flash(_('Successfully deleted pull request'),
288 h.flash(_('Successfully deleted pull request'),
288 category='success')
289 category='success')
289 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
290 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
290 raise HTTPForbidden()
291 raise HTTPForbidden()
291
292
292 def _load_compare_data(self, pull_request, enable_comments=True):
293 def _load_compare_data(self, pull_request, enable_comments=True):
293 """
294 """
294 Load context data needed for generating compare diff
295 Load context data needed for generating compare diff
295
296
296 :param pull_request:
297 :param pull_request:
297 :type pull_request:
298 :type pull_request:
298 """
299 """
299 org_repo = pull_request.org_repo
300 org_repo = pull_request.org_repo
300 (org_ref_type,
301 (org_ref_type,
301 org_ref_name,
302 org_ref_name,
302 org_ref_rev) = pull_request.org_ref.split(':')
303 org_ref_rev) = pull_request.org_ref.split(':')
303
304
304 other_repo = org_repo
305 other_repo = org_repo
305 (other_ref_type,
306 (other_ref_type,
306 other_ref_name,
307 other_ref_name,
307 other_ref_rev) = pull_request.other_ref.split(':')
308 other_ref_rev) = pull_request.other_ref.split(':')
308
309
309 # despite opening revisions for bookmarks/branches/tags, we always
310 # despite opening revisions for bookmarks/branches/tags, we always
310 # convert this to rev to prevent changes after bookmark or branch change
311 # convert this to rev to prevent changes after bookmark or branch change
311 org_ref = ('rev', org_ref_rev)
312 org_ref = ('rev', org_ref_rev)
312 other_ref = ('rev', other_ref_rev)
313 other_ref = ('rev', other_ref_rev)
313
314
314 c.org_repo = org_repo
315 c.org_repo = org_repo
315 c.other_repo = other_repo
316 c.other_repo = other_repo
316
317
317 c.fulldiff = fulldiff = request.GET.get('fulldiff')
318 c.fulldiff = fulldiff = request.GET.get('fulldiff')
318
319
319 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
320 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
320
321
321 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
322 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
322
323
323 c.org_ref = org_ref[1]
324 c.org_ref = org_ref[1]
324 c.org_ref_type = org_ref[0]
325 c.org_ref_type = org_ref[0]
325 c.other_ref = other_ref[1]
326 c.other_ref = other_ref[1]
326 c.other_ref_type = other_ref[0]
327 c.other_ref_type = other_ref[0]
327
328
328 diff_limit = self.cut_off_limit if not fulldiff else None
329 diff_limit = self.cut_off_limit if not fulldiff else None
329
330
330 #we swap org/other ref since we run a simple diff on one repo
331 #we swap org/other ref since we run a simple diff on one repo
331 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
332 log.debug('running diff between %s@%s and %s@%s'
333 % (org_repo.scm_instance.path, org_ref,
334 other_repo.scm_instance.path, other_ref))
335 _diff = org_repo.scm_instance.get_diff(rev1=safe_str(org_ref[1]), rev2=safe_str(other_ref[1]))
332
336
333 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
337 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
334 diff_limit=diff_limit)
338 diff_limit=diff_limit)
335 _parsed = diff_processor.prepare()
339 _parsed = diff_processor.prepare()
336
340
337 c.limited_diff = False
341 c.limited_diff = False
338 if isinstance(_parsed, LimitedDiffContainer):
342 if isinstance(_parsed, LimitedDiffContainer):
339 c.limited_diff = True
343 c.limited_diff = True
340
344
341 c.files = []
345 c.files = []
342 c.changes = {}
346 c.changes = {}
343 c.lines_added = 0
347 c.lines_added = 0
344 c.lines_deleted = 0
348 c.lines_deleted = 0
345 for f in _parsed:
349 for f in _parsed:
346 st = f['stats']
350 st = f['stats']
347 if st[0] != 'b':
351 if st[0] != 'b':
348 c.lines_added += st[0]
352 c.lines_added += st[0]
349 c.lines_deleted += st[1]
353 c.lines_deleted += st[1]
350 fid = h.FID('', f['filename'])
354 fid = h.FID('', f['filename'])
351 c.files.append([fid, f['operation'], f['filename'], f['stats']])
355 c.files.append([fid, f['operation'], f['filename'], f['stats']])
352 diff = diff_processor.as_html(enable_comments=enable_comments,
356 diff = diff_processor.as_html(enable_comments=enable_comments,
353 parsed_lines=[f])
357 parsed_lines=[f])
354 c.changes[fid] = [f['operation'], f['filename'], diff]
358 c.changes[fid] = [f['operation'], f['filename'], diff]
355
359
356 def show(self, repo_name, pull_request_id):
360 def show(self, repo_name, pull_request_id):
357 repo_model = RepoModel()
361 repo_model = RepoModel()
358 c.users_array = repo_model.get_users_js()
362 c.users_array = repo_model.get_users_js()
359 c.users_groups_array = repo_model.get_users_groups_js()
363 c.users_groups_array = repo_model.get_users_groups_js()
360 c.pull_request = PullRequest.get_or_404(pull_request_id)
364 c.pull_request = PullRequest.get_or_404(pull_request_id)
361 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
365 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
362 cc_model = ChangesetCommentsModel()
366 cc_model = ChangesetCommentsModel()
363 cs_model = ChangesetStatusModel()
367 cs_model = ChangesetStatusModel()
364 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
368 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
365 pull_request=c.pull_request,
369 pull_request=c.pull_request,
366 with_revisions=True)
370 with_revisions=True)
367
371
368 cs_statuses = defaultdict(list)
372 cs_statuses = defaultdict(list)
369 for st in _cs_statuses:
373 for st in _cs_statuses:
370 cs_statuses[st.author.username] += [st]
374 cs_statuses[st.author.username] += [st]
371
375
372 c.pull_request_reviewers = []
376 c.pull_request_reviewers = []
373 c.pull_request_pending_reviewers = []
377 c.pull_request_pending_reviewers = []
374 for o in c.pull_request.reviewers:
378 for o in c.pull_request.reviewers:
375 st = cs_statuses.get(o.user.username, None)
379 st = cs_statuses.get(o.user.username, None)
376 if st:
380 if st:
377 sorter = lambda k: k.version
381 sorter = lambda k: k.version
378 st = [(x, list(y)[0])
382 st = [(x, list(y)[0])
379 for x, y in (groupby(sorted(st, key=sorter), sorter))]
383 for x, y in (groupby(sorted(st, key=sorter), sorter))]
380 else:
384 else:
381 c.pull_request_pending_reviewers.append(o.user)
385 c.pull_request_pending_reviewers.append(o.user)
382 c.pull_request_reviewers.append([o.user, st])
386 c.pull_request_reviewers.append([o.user, st])
383
387
384 # pull_requests repo_name we opened it against
388 # pull_requests repo_name we opened it against
385 # ie. other_repo must match
389 # ie. other_repo must match
386 if repo_name != c.pull_request.other_repo.repo_name:
390 if repo_name != c.pull_request.other_repo.repo_name:
387 raise HTTPNotFound
391 raise HTTPNotFound
388
392
389 # load compare data into template context
393 # load compare data into template context
390 enable_comments = not c.pull_request.is_closed()
394 enable_comments = not c.pull_request.is_closed()
391 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
395 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
392
396
393 # inline comments
397 # inline comments
394 c.inline_cnt = 0
398 c.inline_cnt = 0
395 c.inline_comments = cc_model.get_inline_comments(
399 c.inline_comments = cc_model.get_inline_comments(
396 c.rhodecode_db_repo.repo_id,
400 c.rhodecode_db_repo.repo_id,
397 pull_request=pull_request_id)
401 pull_request=pull_request_id)
398 # count inline comments
402 # count inline comments
399 for __, lines in c.inline_comments:
403 for __, lines in c.inline_comments:
400 for comments in lines.values():
404 for comments in lines.values():
401 c.inline_cnt += len(comments)
405 c.inline_cnt += len(comments)
402 # comments
406 # comments
403 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
407 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
404 pull_request=pull_request_id)
408 pull_request=pull_request_id)
405
409
406 try:
410 try:
407 cur_status = c.statuses[c.pull_request.revisions[0]][0]
411 cur_status = c.statuses[c.pull_request.revisions[0]][0]
408 except Exception:
412 except Exception:
409 log.error(traceback.format_exc())
413 log.error(traceback.format_exc())
410 cur_status = 'undefined'
414 cur_status = 'undefined'
411 if c.pull_request.is_closed() and 0:
415 if c.pull_request.is_closed() and 0:
412 c.current_changeset_status = cur_status
416 c.current_changeset_status = cur_status
413 else:
417 else:
414 # changeset(pull-request) status calulation based on reviewers
418 # changeset(pull-request) status calulation based on reviewers
415 c.current_changeset_status = cs_model.calculate_status(
419 c.current_changeset_status = cs_model.calculate_status(
416 c.pull_request_reviewers,
420 c.pull_request_reviewers,
417 )
421 )
418 c.changeset_statuses = ChangesetStatus.STATUSES
422 c.changeset_statuses = ChangesetStatus.STATUSES
419
423
420 c.as_form = False
424 c.as_form = False
421 c.ancestor = None # there is one - but right here we don't know which
425 c.ancestor = None # there is one - but right here we don't know which
422 return render('/pullrequests/pullrequest_show.html')
426 return render('/pullrequests/pullrequest_show.html')
423
427
424 @NotAnonymous()
428 @NotAnonymous()
425 @jsonify
429 @jsonify
426 def comment(self, repo_name, pull_request_id):
430 def comment(self, repo_name, pull_request_id):
427 pull_request = PullRequest.get_or_404(pull_request_id)
431 pull_request = PullRequest.get_or_404(pull_request_id)
428 if pull_request.is_closed():
432 if pull_request.is_closed():
429 raise HTTPForbidden()
433 raise HTTPForbidden()
430
434
431 status = request.POST.get('changeset_status')
435 status = request.POST.get('changeset_status')
432 change_status = request.POST.get('change_changeset_status')
436 change_status = request.POST.get('change_changeset_status')
433 text = request.POST.get('text')
437 text = request.POST.get('text')
434 close_pr = request.POST.get('save_close')
438 close_pr = request.POST.get('save_close')
435
439
436 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
440 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
437 if status and change_status and allowed_to_change_status:
441 if status and change_status and allowed_to_change_status:
438 _def = (_('Status change -> %s')
442 _def = (_('Status change -> %s')
439 % ChangesetStatus.get_status_lbl(status))
443 % ChangesetStatus.get_status_lbl(status))
440 if close_pr:
444 if close_pr:
441 _def = _('Closing with') + ' ' + _def
445 _def = _('Closing with') + ' ' + _def
442 text = text or _def
446 text = text or _def
443 comm = ChangesetCommentsModel().create(
447 comm = ChangesetCommentsModel().create(
444 text=text,
448 text=text,
445 repo=c.rhodecode_db_repo.repo_id,
449 repo=c.rhodecode_db_repo.repo_id,
446 user=c.rhodecode_user.user_id,
450 user=c.rhodecode_user.user_id,
447 pull_request=pull_request_id,
451 pull_request=pull_request_id,
448 f_path=request.POST.get('f_path'),
452 f_path=request.POST.get('f_path'),
449 line_no=request.POST.get('line'),
453 line_no=request.POST.get('line'),
450 status_change=(ChangesetStatus.get_status_lbl(status)
454 status_change=(ChangesetStatus.get_status_lbl(status)
451 if status and change_status
455 if status and change_status
452 and allowed_to_change_status else None),
456 and allowed_to_change_status else None),
453 closing_pr=close_pr
457 closing_pr=close_pr
454 )
458 )
455
459
456 action_logger(self.rhodecode_user,
460 action_logger(self.rhodecode_user,
457 'user_commented_pull_request:%s' % pull_request_id,
461 'user_commented_pull_request:%s' % pull_request_id,
458 c.rhodecode_db_repo, self.ip_addr, self.sa)
462 c.rhodecode_db_repo, self.ip_addr, self.sa)
459
463
460 if allowed_to_change_status:
464 if allowed_to_change_status:
461 # get status if set !
465 # get status if set !
462 if status and change_status:
466 if status and change_status:
463 ChangesetStatusModel().set_status(
467 ChangesetStatusModel().set_status(
464 c.rhodecode_db_repo.repo_id,
468 c.rhodecode_db_repo.repo_id,
465 status,
469 status,
466 c.rhodecode_user.user_id,
470 c.rhodecode_user.user_id,
467 comm,
471 comm,
468 pull_request=pull_request_id
472 pull_request=pull_request_id
469 )
473 )
470
474
471 if close_pr:
475 if close_pr:
472 if status in ['rejected', 'approved']:
476 if status in ['rejected', 'approved']:
473 PullRequestModel().close_pull_request(pull_request_id)
477 PullRequestModel().close_pull_request(pull_request_id)
474 action_logger(self.rhodecode_user,
478 action_logger(self.rhodecode_user,
475 'user_closed_pull_request:%s' % pull_request_id,
479 'user_closed_pull_request:%s' % pull_request_id,
476 c.rhodecode_db_repo, self.ip_addr, self.sa)
480 c.rhodecode_db_repo, self.ip_addr, self.sa)
477 else:
481 else:
478 h.flash(_('Closing pull request on other statuses than '
482 h.flash(_('Closing pull request on other statuses than '
479 'rejected or approved forbidden'),
483 'rejected or approved forbidden'),
480 category='warning')
484 category='warning')
481
485
482 Session().commit()
486 Session().commit()
483
487
484 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
488 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
485 return redirect(h.url('pullrequest_show', repo_name=repo_name,
489 return redirect(h.url('pullrequest_show', repo_name=repo_name,
486 pull_request_id=pull_request_id))
490 pull_request_id=pull_request_id))
487
491
488 data = {
492 data = {
489 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
493 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
490 }
494 }
491 if comm:
495 if comm:
492 c.co = comm
496 c.co = comm
493 data.update(comm.get_dict())
497 data.update(comm.get_dict())
494 data.update({'rendered_text':
498 data.update({'rendered_text':
495 render('changeset/changeset_comment_block.html')})
499 render('changeset/changeset_comment_block.html')})
496
500
497 return data
501 return data
498
502
499 @NotAnonymous()
503 @NotAnonymous()
500 @jsonify
504 @jsonify
501 def delete_comment(self, repo_name, comment_id):
505 def delete_comment(self, repo_name, comment_id):
502 co = ChangesetComment.get(comment_id)
506 co = ChangesetComment.get(comment_id)
503 if co.pull_request.is_closed():
507 if co.pull_request.is_closed():
504 #don't allow deleting comments on closed pull request
508 #don't allow deleting comments on closed pull request
505 raise HTTPForbidden()
509 raise HTTPForbidden()
506
510
507 owner = co.author.user_id == c.rhodecode_user.user_id
511 owner = co.author.user_id == c.rhodecode_user.user_id
508 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
512 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
509 ChangesetCommentsModel().delete(comment=co)
513 ChangesetCommentsModel().delete(comment=co)
510 Session().commit()
514 Session().commit()
511 return True
515 return True
512 else:
516 else:
513 raise HTTPForbidden()
517 raise HTTPForbidden()
@@ -1,717 +1,684 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import logging
30 import logging
31
31
32 from itertools import tee, imap
32 from itertools import tee, imap
33
33
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35
35
36 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
38 from rhodecode.lib.vcs.backends.base import EmptyChangeset
39 from rhodecode.lib.helpers import escape
39 from rhodecode.lib.helpers import escape
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def wrap_to_table(str_):
45 def wrap_to_table(str_):
46 return '''<table class="code-difftable">
46 return '''<table class="code-difftable">
47 <tr class="line no-comment">
47 <tr class="line no-comment">
48 <td class="lineno new"></td>
48 <td class="lineno new"></td>
49 <td class="code no-comment"><pre>%s</pre></td>
49 <td class="code no-comment"><pre>%s</pre></td>
50 </tr>
50 </tr>
51 </table>''' % str_
51 </table>''' % str_
52
52
53
53
54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
54 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
55 ignore_whitespace=True, line_context=3,
55 ignore_whitespace=True, line_context=3,
56 enable_comments=False):
56 enable_comments=False):
57 """
57 """
58 returns a wrapped diff into a table, checks for cut_off_limit and presents
58 returns a wrapped diff into a table, checks for cut_off_limit and presents
59 proper message
59 proper message
60 """
60 """
61
61
62 if filenode_old is None:
62 if filenode_old is None:
63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
63 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
64
64
65 if filenode_old.is_binary or filenode_new.is_binary:
65 if filenode_old.is_binary or filenode_new.is_binary:
66 diff = wrap_to_table(_('Binary file'))
66 diff = wrap_to_table(_('Binary file'))
67 stats = (0, 0)
67 stats = (0, 0)
68 size = 0
68 size = 0
69
69
70 elif cut_off_limit != -1 and (cut_off_limit is None or
70 elif cut_off_limit != -1 and (cut_off_limit is None or
71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
71 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
72
72
73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
73 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
74 ignore_whitespace=ignore_whitespace,
74 ignore_whitespace=ignore_whitespace,
75 context=line_context)
75 context=line_context)
76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
76 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
77
77
78 diff = diff_processor.as_html(enable_comments=enable_comments)
78 diff = diff_processor.as_html(enable_comments=enable_comments)
79 stats = diff_processor.stat()
79 stats = diff_processor.stat()
80 size = len(diff or '')
80 size = len(diff or '')
81 else:
81 else:
82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
82 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
83 'diff menu to display this diff'))
83 'diff menu to display this diff'))
84 stats = (0, 0)
84 stats = (0, 0)
85 size = 0
85 size = 0
86 if not diff:
86 if not diff:
87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
87 submodules = filter(lambda o: isinstance(o, SubModuleNode),
88 [filenode_new, filenode_old])
88 [filenode_new, filenode_old])
89 if submodules:
89 if submodules:
90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
90 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
91 else:
91 else:
92 diff = wrap_to_table(_('No changes detected'))
92 diff = wrap_to_table(_('No changes detected'))
93
93
94 cs1 = filenode_old.changeset.raw_id
94 cs1 = filenode_old.changeset.raw_id
95 cs2 = filenode_new.changeset.raw_id
95 cs2 = filenode_new.changeset.raw_id
96
96
97 return size, cs1, cs2, diff, stats
97 return size, cs1, cs2, diff, stats
98
98
99
99
100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
100 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
101 """
101 """
102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
102 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
103
103
104 :param ignore_whitespace: ignore whitespaces in diff
104 :param ignore_whitespace: ignore whitespaces in diff
105 """
105 """
106 # make sure we pass in default context
106 # make sure we pass in default context
107 context = context or 3
107 context = context or 3
108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
108 submodules = filter(lambda o: isinstance(o, SubModuleNode),
109 [filenode_new, filenode_old])
109 [filenode_new, filenode_old])
110 if submodules:
110 if submodules:
111 return ''
111 return ''
112
112
113 for filenode in (filenode_old, filenode_new):
113 for filenode in (filenode_old, filenode_new):
114 if not isinstance(filenode, FileNode):
114 if not isinstance(filenode, FileNode):
115 raise VCSError("Given object should be FileNode object, not %s"
115 raise VCSError("Given object should be FileNode object, not %s"
116 % filenode.__class__)
116 % filenode.__class__)
117
117
118 repo = filenode_new.changeset.repository
118 repo = filenode_new.changeset.repository
119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
119 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
120 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
121
121
122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
122 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
123 ignore_whitespace, context)
123 ignore_whitespace, context)
124 return vcs_gitdiff
124 return vcs_gitdiff
125
125
126 NEW_FILENODE = 1
126 NEW_FILENODE = 1
127 DEL_FILENODE = 2
127 DEL_FILENODE = 2
128 MOD_FILENODE = 3
128 MOD_FILENODE = 3
129 RENAMED_FILENODE = 4
129 RENAMED_FILENODE = 4
130 CHMOD_FILENODE = 5
130 CHMOD_FILENODE = 5
131
131
132
132
133 class DiffLimitExceeded(Exception):
133 class DiffLimitExceeded(Exception):
134 pass
134 pass
135
135
136
136
137 class LimitedDiffContainer(object):
137 class LimitedDiffContainer(object):
138
138
139 def __init__(self, diff_limit, cur_diff_size, diff):
139 def __init__(self, diff_limit, cur_diff_size, diff):
140 self.diff = diff
140 self.diff = diff
141 self.diff_limit = diff_limit
141 self.diff_limit = diff_limit
142 self.cur_diff_size = cur_diff_size
142 self.cur_diff_size = cur_diff_size
143
143
144 def __iter__(self):
144 def __iter__(self):
145 for l in self.diff:
145 for l in self.diff:
146 yield l
146 yield l
147
147
148
148
149 class DiffProcessor(object):
149 class DiffProcessor(object):
150 """
150 """
151 Give it a unified or git diff and it returns a list of the files that were
151 Give it a unified or git diff and it returns a list of the files that were
152 mentioned in the diff together with a dict of meta information that
152 mentioned in the diff together with a dict of meta information that
153 can be used to render it in a HTML template.
153 can be used to render it in a HTML template.
154 """
154 """
155 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
155 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
156 _newline_marker = re.compile(r'^\\ No newline at end of file')
156 _newline_marker = re.compile(r'^\\ No newline at end of file')
157 _git_header_re = re.compile(r"""
157 _git_header_re = re.compile(r"""
158 #^diff[ ]--git
158 #^diff[ ]--git
159 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
159 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
160 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
160 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
161 ^rename[ ]from[ ](?P<rename_from>\S+)\n
161 ^rename[ ]from[ ](?P<rename_from>\S+)\n
162 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
162 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
163 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
163 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
164 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
164 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
165 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
165 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
166 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
166 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
167 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
167 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
168 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
168 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
169 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
169 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
170 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
170 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
171 """, re.VERBOSE | re.MULTILINE)
171 """, re.VERBOSE | re.MULTILINE)
172 _hg_header_re = re.compile(r"""
172 _hg_header_re = re.compile(r"""
173 #^diff[ ]--git
173 #^diff[ ]--git
174 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
174 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
175 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
175 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
176 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
176 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
177 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
177 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
178 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
178 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
179 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
179 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
180 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
180 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
181 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
181 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
182 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
182 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
183 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
183 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
184 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
184 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
185 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
185 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
186 """, re.VERBOSE | re.MULTILINE)
186 """, re.VERBOSE | re.MULTILINE)
187
187
188 #used for inline highlighter word split
188 #used for inline highlighter word split
189 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
189 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
190
190
191 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
191 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
192 """
192 """
193 :param diff: a text in diff format
193 :param diff: a text in diff format
194 :param vcs: type of version controll hg or git
194 :param vcs: type of version controll hg or git
195 :param format: format of diff passed, `udiff` or `gitdiff`
195 :param format: format of diff passed, `udiff` or `gitdiff`
196 :param diff_limit: define the size of diff that is considered "big"
196 :param diff_limit: define the size of diff that is considered "big"
197 based on that parameter cut off will be triggered, set to None
197 based on that parameter cut off will be triggered, set to None
198 to show full diff
198 to show full diff
199 """
199 """
200 if not isinstance(diff, basestring):
200 if not isinstance(diff, basestring):
201 raise Exception('Diff must be a basestring got %s instead' % type(diff))
201 raise Exception('Diff must be a basestring got %s instead' % type(diff))
202
202
203 self._diff = diff
203 self._diff = diff
204 self._format = format
204 self._format = format
205 self.adds = 0
205 self.adds = 0
206 self.removes = 0
206 self.removes = 0
207 # calculate diff size
207 # calculate diff size
208 self.diff_size = len(diff)
208 self.diff_size = len(diff)
209 self.diff_limit = diff_limit
209 self.diff_limit = diff_limit
210 self.cur_diff_size = 0
210 self.cur_diff_size = 0
211 self.parsed = False
211 self.parsed = False
212 self.parsed_diff = []
212 self.parsed_diff = []
213 self.vcs = vcs
213 self.vcs = vcs
214
214
215 if format == 'gitdiff':
215 if format == 'gitdiff':
216 self.differ = self._highlight_line_difflib
216 self.differ = self._highlight_line_difflib
217 self._parser = self._parse_gitdiff
217 self._parser = self._parse_gitdiff
218 else:
218 else:
219 self.differ = self._highlight_line_udiff
219 self.differ = self._highlight_line_udiff
220 self._parser = self._parse_udiff
220 self._parser = self._parse_udiff
221
221
222 def _copy_iterator(self):
222 def _copy_iterator(self):
223 """
223 """
224 make a fresh copy of generator, we should not iterate thru
224 make a fresh copy of generator, we should not iterate thru
225 an original as it's needed for repeating operations on
225 an original as it's needed for repeating operations on
226 this instance of DiffProcessor
226 this instance of DiffProcessor
227 """
227 """
228 self.__udiff, iterator_copy = tee(self.__udiff)
228 self.__udiff, iterator_copy = tee(self.__udiff)
229 return iterator_copy
229 return iterator_copy
230
230
231 def _escaper(self, string):
231 def _escaper(self, string):
232 """
232 """
233 Escaper for diff escapes special chars and checks the diff limit
233 Escaper for diff escapes special chars and checks the diff limit
234
234
235 :param string:
235 :param string:
236 :type string:
236 :type string:
237 """
237 """
238
238
239 self.cur_diff_size += len(string)
239 self.cur_diff_size += len(string)
240
240
241 # escaper get's iterated on each .next() call and it checks if each
241 # escaper get's iterated on each .next() call and it checks if each
242 # parsed line doesn't exceed the diff limit
242 # parsed line doesn't exceed the diff limit
243 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
243 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
244 raise DiffLimitExceeded('Diff Limit Exceeded')
244 raise DiffLimitExceeded('Diff Limit Exceeded')
245
245
246 return safe_unicode(string).replace('&', '&amp;')\
246 return safe_unicode(string).replace('&', '&amp;')\
247 .replace('<', '&lt;')\
247 .replace('<', '&lt;')\
248 .replace('>', '&gt;')
248 .replace('>', '&gt;')
249
249
250 def _line_counter(self, l):
250 def _line_counter(self, l):
251 """
251 """
252 Checks each line and bumps total adds/removes for this diff
252 Checks each line and bumps total adds/removes for this diff
253
253
254 :param l:
254 :param l:
255 """
255 """
256 if l.startswith('+') and not l.startswith('+++'):
256 if l.startswith('+') and not l.startswith('+++'):
257 self.adds += 1
257 self.adds += 1
258 elif l.startswith('-') and not l.startswith('---'):
258 elif l.startswith('-') and not l.startswith('---'):
259 self.removes += 1
259 self.removes += 1
260 return safe_unicode(l)
260 return safe_unicode(l)
261
261
262 def _highlight_line_difflib(self, line, next_):
262 def _highlight_line_difflib(self, line, next_):
263 """
263 """
264 Highlight inline changes in both lines.
264 Highlight inline changes in both lines.
265 """
265 """
266
266
267 if line['action'] == 'del':
267 if line['action'] == 'del':
268 old, new = line, next_
268 old, new = line, next_
269 else:
269 else:
270 old, new = next_, line
270 old, new = next_, line
271
271
272 oldwords = self._token_re.split(old['line'])
272 oldwords = self._token_re.split(old['line'])
273 newwords = self._token_re.split(new['line'])
273 newwords = self._token_re.split(new['line'])
274 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
274 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
275
275
276 oldfragments, newfragments = [], []
276 oldfragments, newfragments = [], []
277 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
277 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
278 oldfrag = ''.join(oldwords[i1:i2])
278 oldfrag = ''.join(oldwords[i1:i2])
279 newfrag = ''.join(newwords[j1:j2])
279 newfrag = ''.join(newwords[j1:j2])
280 if tag != 'equal':
280 if tag != 'equal':
281 if oldfrag:
281 if oldfrag:
282 oldfrag = '<del>%s</del>' % oldfrag
282 oldfrag = '<del>%s</del>' % oldfrag
283 if newfrag:
283 if newfrag:
284 newfrag = '<ins>%s</ins>' % newfrag
284 newfrag = '<ins>%s</ins>' % newfrag
285 oldfragments.append(oldfrag)
285 oldfragments.append(oldfrag)
286 newfragments.append(newfrag)
286 newfragments.append(newfrag)
287
287
288 old['line'] = "".join(oldfragments)
288 old['line'] = "".join(oldfragments)
289 new['line'] = "".join(newfragments)
289 new['line'] = "".join(newfragments)
290
290
291 def _highlight_line_udiff(self, line, next_):
291 def _highlight_line_udiff(self, line, next_):
292 """
292 """
293 Highlight inline changes in both lines.
293 Highlight inline changes in both lines.
294 """
294 """
295 start = 0
295 start = 0
296 limit = min(len(line['line']), len(next_['line']))
296 limit = min(len(line['line']), len(next_['line']))
297 while start < limit and line['line'][start] == next_['line'][start]:
297 while start < limit and line['line'][start] == next_['line'][start]:
298 start += 1
298 start += 1
299 end = -1
299 end = -1
300 limit -= start
300 limit -= start
301 while -end <= limit and line['line'][end] == next_['line'][end]:
301 while -end <= limit and line['line'][end] == next_['line'][end]:
302 end -= 1
302 end -= 1
303 end += 1
303 end += 1
304 if start or end:
304 if start or end:
305 def do(l):
305 def do(l):
306 last = end + len(l['line'])
306 last = end + len(l['line'])
307 if l['action'] == 'add':
307 if l['action'] == 'add':
308 tag = 'ins'
308 tag = 'ins'
309 else:
309 else:
310 tag = 'del'
310 tag = 'del'
311 l['line'] = '%s<%s>%s</%s>%s' % (
311 l['line'] = '%s<%s>%s</%s>%s' % (
312 l['line'][:start],
312 l['line'][:start],
313 tag,
313 tag,
314 l['line'][start:last],
314 l['line'][start:last],
315 tag,
315 tag,
316 l['line'][last:]
316 l['line'][last:]
317 )
317 )
318 do(line)
318 do(line)
319 do(next_)
319 do(next_)
320
320
321 def _get_header(self, diff_chunk):
321 def _get_header(self, diff_chunk):
322 """
322 """
323 parses the diff header, and returns parts, and leftover diff
323 parses the diff header, and returns parts, and leftover diff
324 parts consists of 14 elements::
324 parts consists of 14 elements::
325
325
326 a_path, b_path, similarity_index, rename_from, rename_to,
326 a_path, b_path, similarity_index, rename_from, rename_to,
327 old_mode, new_mode, new_file_mode, deleted_file_mode,
327 old_mode, new_mode, new_file_mode, deleted_file_mode,
328 a_blob_id, b_blob_id, b_mode, a_file, b_file
328 a_blob_id, b_blob_id, b_mode, a_file, b_file
329
329
330 :param diff_chunk:
330 :param diff_chunk:
331 :type diff_chunk:
331 :type diff_chunk:
332 """
332 """
333
333
334 if self.vcs == 'git':
334 if self.vcs == 'git':
335 match = self._git_header_re.match(diff_chunk)
335 match = self._git_header_re.match(diff_chunk)
336 diff = diff_chunk[match.end():]
336 diff = diff_chunk[match.end():]
337 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
337 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
338 elif self.vcs == 'hg':
338 elif self.vcs == 'hg':
339 match = self._hg_header_re.match(diff_chunk)
339 match = self._hg_header_re.match(diff_chunk)
340 diff = diff_chunk[match.end():]
340 diff = diff_chunk[match.end():]
341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
341 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
342 else:
342 else:
343 raise Exception('VCS type %s is not supported' % self.vcs)
343 raise Exception('VCS type %s is not supported' % self.vcs)
344
344
345 def _clean_line(self, line, command):
345 def _clean_line(self, line, command):
346 if command in ['+', '-', ' ']:
346 if command in ['+', '-', ' ']:
347 #only modify the line if it's actually a diff thing
347 #only modify the line if it's actually a diff thing
348 line = line[1:]
348 line = line[1:]
349 return line
349 return line
350
350
351 def _parse_gitdiff(self, inline_diff=True):
351 def _parse_gitdiff(self, inline_diff=True):
352 _files = []
352 _files = []
353 diff_container = lambda arg: arg
353 diff_container = lambda arg: arg
354
354
355 ##split the diff in chunks of separate --git a/file b/file chunks
355 ##split the diff in chunks of separate --git a/file b/file chunks
356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 binary = False
357 binary = False
358 binary_msg = 'unknown binary'
358 binary_msg = 'unknown binary'
359 head, diff = self._get_header(raw_diff)
359 head, diff = self._get_header(raw_diff)
360
360
361 if not head['a_file'] and head['b_file']:
361 if not head['a_file'] and head['b_file']:
362 op = 'A'
362 op = 'A'
363 elif head['a_file'] and head['b_file']:
363 elif head['a_file'] and head['b_file']:
364 op = 'M'
364 op = 'M'
365 elif head['a_file'] and not head['b_file']:
365 elif head['a_file'] and not head['b_file']:
366 op = 'D'
366 op = 'D'
367 else:
367 else:
368 #probably we're dealing with a binary file 1
368 #probably we're dealing with a binary file 1
369 binary = True
369 binary = True
370 if head['deleted_file_mode']:
370 if head['deleted_file_mode']:
371 op = 'D'
371 op = 'D'
372 stats = ['b', DEL_FILENODE]
372 stats = ['b', DEL_FILENODE]
373 binary_msg = 'deleted binary file'
373 binary_msg = 'deleted binary file'
374 elif head['new_file_mode']:
374 elif head['new_file_mode']:
375 op = 'A'
375 op = 'A'
376 stats = ['b', NEW_FILENODE]
376 stats = ['b', NEW_FILENODE]
377 binary_msg = 'new binary file %s' % head['new_file_mode']
377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 else:
378 else:
379 if head['new_mode'] and head['old_mode']:
379 if head['new_mode'] and head['old_mode']:
380 stats = ['b', CHMOD_FILENODE]
380 stats = ['b', CHMOD_FILENODE]
381 op = 'M'
381 op = 'M'
382 binary_msg = ('modified binary file chmod %s => %s'
382 binary_msg = ('modified binary file chmod %s => %s'
383 % (head['old_mode'], head['new_mode']))
383 % (head['old_mode'], head['new_mode']))
384 elif (head['rename_from'] and head['rename_to']
384 elif (head['rename_from'] and head['rename_to']
385 and head['rename_from'] != head['rename_to']):
385 and head['rename_from'] != head['rename_to']):
386 stats = ['b', RENAMED_FILENODE]
386 stats = ['b', RENAMED_FILENODE]
387 op = 'M'
387 op = 'M'
388 binary_msg = ('file renamed from %s to %s'
388 binary_msg = ('file renamed from %s to %s'
389 % (head['rename_from'], head['rename_to']))
389 % (head['rename_from'], head['rename_to']))
390 else:
390 else:
391 stats = ['b', MOD_FILENODE]
391 stats = ['b', MOD_FILENODE]
392 op = 'M'
392 op = 'M'
393 binary_msg = 'modified binary file'
393 binary_msg = 'modified binary file'
394
394
395 if not binary:
395 if not binary:
396 try:
396 try:
397 chunks, stats = self._parse_lines(diff)
397 chunks, stats = self._parse_lines(diff)
398 except DiffLimitExceeded:
398 except DiffLimitExceeded:
399 diff_container = lambda _diff: LimitedDiffContainer(
399 diff_container = lambda _diff: LimitedDiffContainer(
400 self.diff_limit,
400 self.diff_limit,
401 self.cur_diff_size,
401 self.cur_diff_size,
402 _diff)
402 _diff)
403 break
403 break
404 else:
404 else:
405 chunks = []
405 chunks = []
406 chunks.append([{
406 chunks.append([{
407 'old_lineno': '',
407 'old_lineno': '',
408 'new_lineno': '',
408 'new_lineno': '',
409 'action': 'binary',
409 'action': 'binary',
410 'line': binary_msg,
410 'line': binary_msg,
411 }])
411 }])
412
412
413 _files.append({
413 _files.append({
414 'filename': head['b_path'],
414 'filename': head['b_path'],
415 'old_revision': head['a_blob_id'],
415 'old_revision': head['a_blob_id'],
416 'new_revision': head['b_blob_id'],
416 'new_revision': head['b_blob_id'],
417 'chunks': chunks,
417 'chunks': chunks,
418 'operation': op,
418 'operation': op,
419 'stats': stats,
419 'stats': stats,
420 })
420 })
421
421
422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423
423
424 if not inline_diff:
424 if not inline_diff:
425 return diff_container(sorted(_files, key=sorter))
425 return diff_container(sorted(_files, key=sorter))
426
426
427 # highlight inline changes
427 # highlight inline changes
428 for diff_data in _files:
428 for diff_data in _files:
429 for chunk in diff_data['chunks']:
429 for chunk in diff_data['chunks']:
430 lineiter = iter(chunk)
430 lineiter = iter(chunk)
431 try:
431 try:
432 while 1:
432 while 1:
433 line = lineiter.next()
433 line = lineiter.next()
434 if line['action'] not in ['unmod', 'context']:
434 if line['action'] not in ['unmod', 'context']:
435 nextline = lineiter.next()
435 nextline = lineiter.next()
436 if nextline['action'] in ['unmod', 'context'] or \
436 if nextline['action'] in ['unmod', 'context'] or \
437 nextline['action'] == line['action']:
437 nextline['action'] == line['action']:
438 continue
438 continue
439 self.differ(line, nextline)
439 self.differ(line, nextline)
440 except StopIteration:
440 except StopIteration:
441 pass
441 pass
442
442
443 return diff_container(sorted(_files, key=sorter))
443 return diff_container(sorted(_files, key=sorter))
444
444
445 def _parse_udiff(self, inline_diff=True):
445 def _parse_udiff(self, inline_diff=True):
446 raise NotImplementedError()
446 raise NotImplementedError()
447
447
448 def _parse_lines(self, diff):
448 def _parse_lines(self, diff):
449 """
449 """
450 Parse the diff an return data for the template.
450 Parse the diff an return data for the template.
451 """
451 """
452
452
453 lineiter = iter(diff)
453 lineiter = iter(diff)
454 stats = [0, 0]
454 stats = [0, 0]
455
455
456 try:
456 try:
457 chunks = []
457 chunks = []
458 line = lineiter.next()
458 line = lineiter.next()
459
459
460 while line:
460 while line:
461 lines = []
461 lines = []
462 chunks.append(lines)
462 chunks.append(lines)
463
463
464 match = self._chunk_re.match(line)
464 match = self._chunk_re.match(line)
465
465
466 if not match:
466 if not match:
467 break
467 break
468
468
469 gr = match.groups()
469 gr = match.groups()
470 (old_line, old_end,
470 (old_line, old_end,
471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 old_line -= 1
472 old_line -= 1
473 new_line -= 1
473 new_line -= 1
474
474
475 context = len(gr) == 5
475 context = len(gr) == 5
476 old_end += old_line
476 old_end += old_line
477 new_end += new_line
477 new_end += new_line
478
478
479 if context:
479 if context:
480 # skip context only if it's first line
480 # skip context only if it's first line
481 if int(gr[0]) > 1:
481 if int(gr[0]) > 1:
482 lines.append({
482 lines.append({
483 'old_lineno': '...',
483 'old_lineno': '...',
484 'new_lineno': '...',
484 'new_lineno': '...',
485 'action': 'context',
485 'action': 'context',
486 'line': line,
486 'line': line,
487 })
487 })
488
488
489 line = lineiter.next()
489 line = lineiter.next()
490
490
491 while old_line < old_end or new_line < new_end:
491 while old_line < old_end or new_line < new_end:
492 command = ' '
492 command = ' '
493 if line:
493 if line:
494 command = line[0]
494 command = line[0]
495
495
496 affects_old = affects_new = False
496 affects_old = affects_new = False
497
497
498 # ignore those if we don't expect them
498 # ignore those if we don't expect them
499 if command in '#@':
499 if command in '#@':
500 continue
500 continue
501 elif command == '+':
501 elif command == '+':
502 affects_new = True
502 affects_new = True
503 action = 'add'
503 action = 'add'
504 stats[0] += 1
504 stats[0] += 1
505 elif command == '-':
505 elif command == '-':
506 affects_old = True
506 affects_old = True
507 action = 'del'
507 action = 'del'
508 stats[1] += 1
508 stats[1] += 1
509 else:
509 else:
510 affects_old = affects_new = True
510 affects_old = affects_new = True
511 action = 'unmod'
511 action = 'unmod'
512
512
513 if not self._newline_marker.match(line):
513 if not self._newline_marker.match(line):
514 old_line += affects_old
514 old_line += affects_old
515 new_line += affects_new
515 new_line += affects_new
516 lines.append({
516 lines.append({
517 'old_lineno': affects_old and old_line or '',
517 'old_lineno': affects_old and old_line or '',
518 'new_lineno': affects_new and new_line or '',
518 'new_lineno': affects_new and new_line or '',
519 'action': action,
519 'action': action,
520 'line': self._clean_line(line, command)
520 'line': self._clean_line(line, command)
521 })
521 })
522
522
523 line = lineiter.next()
523 line = lineiter.next()
524
524
525 if self._newline_marker.match(line):
525 if self._newline_marker.match(line):
526 # we need to append to lines, since this is not
526 # we need to append to lines, since this is not
527 # counted in the line specs of diff
527 # counted in the line specs of diff
528 lines.append({
528 lines.append({
529 'old_lineno': '...',
529 'old_lineno': '...',
530 'new_lineno': '...',
530 'new_lineno': '...',
531 'action': 'context',
531 'action': 'context',
532 'line': self._clean_line(line, command)
532 'line': self._clean_line(line, command)
533 })
533 })
534
534
535 except StopIteration:
535 except StopIteration:
536 pass
536 pass
537 return chunks, stats
537 return chunks, stats
538
538
539 def _safe_id(self, idstring):
539 def _safe_id(self, idstring):
540 """Make a string safe for including in an id attribute.
540 """Make a string safe for including in an id attribute.
541
541
542 The HTML spec says that id attributes 'must begin with
542 The HTML spec says that id attributes 'must begin with
543 a letter ([A-Za-z]) and may be followed by any number
543 a letter ([A-Za-z]) and may be followed by any number
544 of letters, digits ([0-9]), hyphens ("-"), underscores
544 of letters, digits ([0-9]), hyphens ("-"), underscores
545 ("_"), colons (":"), and periods (".")'. These regexps
545 ("_"), colons (":"), and periods (".")'. These regexps
546 are slightly over-zealous, in that they remove colons
546 are slightly over-zealous, in that they remove colons
547 and periods unnecessarily.
547 and periods unnecessarily.
548
548
549 Whitespace is transformed into underscores, and then
549 Whitespace is transformed into underscores, and then
550 anything which is not a hyphen or a character that
550 anything which is not a hyphen or a character that
551 matches \w (alphanumerics and underscore) is removed.
551 matches \w (alphanumerics and underscore) is removed.
552
552
553 """
553 """
554 # Transform all whitespace to underscore
554 # Transform all whitespace to underscore
555 idstring = re.sub(r'\s', "_", '%s' % idstring)
555 idstring = re.sub(r'\s', "_", '%s' % idstring)
556 # Remove everything that is not a hyphen or a member of \w
556 # Remove everything that is not a hyphen or a member of \w
557 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
557 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
558 return idstring
558 return idstring
559
559
560 def prepare(self, inline_diff=True):
560 def prepare(self, inline_diff=True):
561 """
561 """
562 Prepare the passed udiff for HTML rendering. It'l return a list
562 Prepare the passed udiff for HTML rendering. It'l return a list
563 of dicts with diff information
563 of dicts with diff information
564 """
564 """
565 parsed = self._parser(inline_diff=inline_diff)
565 parsed = self._parser(inline_diff=inline_diff)
566 self.parsed = True
566 self.parsed = True
567 self.parsed_diff = parsed
567 self.parsed_diff = parsed
568 return parsed
568 return parsed
569
569
570 def as_raw(self, diff_lines=None):
570 def as_raw(self, diff_lines=None):
571 """
571 """
572 Returns raw string diff
572 Returns raw string diff
573 """
573 """
574 return self._diff
574 return self._diff
575 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
575 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
576
576
577 def as_html(self, table_class='code-difftable', line_class='line',
577 def as_html(self, table_class='code-difftable', line_class='line',
578 old_lineno_class='lineno old', new_lineno_class='lineno new',
578 old_lineno_class='lineno old', new_lineno_class='lineno new',
579 code_class='code', enable_comments=False, parsed_lines=None):
579 code_class='code', enable_comments=False, parsed_lines=None):
580 """
580 """
581 Return given diff as html table with customized css classes
581 Return given diff as html table with customized css classes
582 """
582 """
583 def _link_to_if(condition, label, url):
583 def _link_to_if(condition, label, url):
584 """
584 """
585 Generates a link if condition is meet or just the label if not.
585 Generates a link if condition is meet or just the label if not.
586 """
586 """
587
587
588 if condition:
588 if condition:
589 return '''<a href="%(url)s">%(label)s</a>''' % {
589 return '''<a href="%(url)s">%(label)s</a>''' % {
590 'url': url,
590 'url': url,
591 'label': label
591 'label': label
592 }
592 }
593 else:
593 else:
594 return label
594 return label
595 if not self.parsed:
595 if not self.parsed:
596 self.prepare()
596 self.prepare()
597
597
598 diff_lines = self.parsed_diff
598 diff_lines = self.parsed_diff
599 if parsed_lines:
599 if parsed_lines:
600 diff_lines = parsed_lines
600 diff_lines = parsed_lines
601
601
602 _html_empty = True
602 _html_empty = True
603 _html = []
603 _html = []
604 _html.append('''<table class="%(table_class)s">\n''' % {
604 _html.append('''<table class="%(table_class)s">\n''' % {
605 'table_class': table_class
605 'table_class': table_class
606 })
606 })
607
607
608 for diff in diff_lines:
608 for diff in diff_lines:
609 for line in diff['chunks']:
609 for line in diff['chunks']:
610 _html_empty = False
610 _html_empty = False
611 for change in line:
611 for change in line:
612 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
612 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
613 'lc': line_class,
613 'lc': line_class,
614 'action': change['action']
614 'action': change['action']
615 })
615 })
616 anchor_old_id = ''
616 anchor_old_id = ''
617 anchor_new_id = ''
617 anchor_new_id = ''
618 anchor_old = "%(filename)s_o%(oldline_no)s" % {
618 anchor_old = "%(filename)s_o%(oldline_no)s" % {
619 'filename': self._safe_id(diff['filename']),
619 'filename': self._safe_id(diff['filename']),
620 'oldline_no': change['old_lineno']
620 'oldline_no': change['old_lineno']
621 }
621 }
622 anchor_new = "%(filename)s_n%(oldline_no)s" % {
622 anchor_new = "%(filename)s_n%(oldline_no)s" % {
623 'filename': self._safe_id(diff['filename']),
623 'filename': self._safe_id(diff['filename']),
624 'oldline_no': change['new_lineno']
624 'oldline_no': change['new_lineno']
625 }
625 }
626 cond_old = (change['old_lineno'] != '...' and
626 cond_old = (change['old_lineno'] != '...' and
627 change['old_lineno'])
627 change['old_lineno'])
628 cond_new = (change['new_lineno'] != '...' and
628 cond_new = (change['new_lineno'] != '...' and
629 change['new_lineno'])
629 change['new_lineno'])
630 if cond_old:
630 if cond_old:
631 anchor_old_id = 'id="%s"' % anchor_old
631 anchor_old_id = 'id="%s"' % anchor_old
632 if cond_new:
632 if cond_new:
633 anchor_new_id = 'id="%s"' % anchor_new
633 anchor_new_id = 'id="%s"' % anchor_new
634 ###########################################################
634 ###########################################################
635 # OLD LINE NUMBER
635 # OLD LINE NUMBER
636 ###########################################################
636 ###########################################################
637 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
637 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
638 'a_id': anchor_old_id,
638 'a_id': anchor_old_id,
639 'olc': old_lineno_class
639 'olc': old_lineno_class
640 })
640 })
641
641
642 _html.append('''%(link)s''' % {
642 _html.append('''%(link)s''' % {
643 'link': _link_to_if(True, change['old_lineno'],
643 'link': _link_to_if(True, change['old_lineno'],
644 '#%s' % anchor_old)
644 '#%s' % anchor_old)
645 })
645 })
646 _html.append('''</td>\n''')
646 _html.append('''</td>\n''')
647 ###########################################################
647 ###########################################################
648 # NEW LINE NUMBER
648 # NEW LINE NUMBER
649 ###########################################################
649 ###########################################################
650
650
651 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
651 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
652 'a_id': anchor_new_id,
652 'a_id': anchor_new_id,
653 'nlc': new_lineno_class
653 'nlc': new_lineno_class
654 })
654 })
655
655
656 _html.append('''%(link)s''' % {
656 _html.append('''%(link)s''' % {
657 'link': _link_to_if(True, change['new_lineno'],
657 'link': _link_to_if(True, change['new_lineno'],
658 '#%s' % anchor_new)
658 '#%s' % anchor_new)
659 })
659 })
660 _html.append('''</td>\n''')
660 _html.append('''</td>\n''')
661 ###########################################################
661 ###########################################################
662 # CODE
662 # CODE
663 ###########################################################
663 ###########################################################
664 comments = '' if enable_comments else 'no-comment'
664 comments = '' if enable_comments else 'no-comment'
665 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
665 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
666 'cc': code_class,
666 'cc': code_class,
667 'inc': comments
667 'inc': comments
668 })
668 })
669 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
669 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
670 'code': change['line']
670 'code': change['line']
671 })
671 })
672
672
673 _html.append('''\t</td>''')
673 _html.append('''\t</td>''')
674 _html.append('''\n</tr>\n''')
674 _html.append('''\n</tr>\n''')
675 _html.append('''</table>''')
675 _html.append('''</table>''')
676 if _html_empty:
676 if _html_empty:
677 return None
677 return None
678 return ''.join(_html)
678 return ''.join(_html)
679
679
680 def stat(self):
680 def stat(self):
681 """
681 """
682 Returns tuple of added, and removed lines for this instance
682 Returns tuple of added, and removed lines for this instance
683 """
683 """
684 return self.adds, self.removes
684 return self.adds, self.removes
685
686
687 def differ(org_repo, org_ref, other_repo, other_ref,
688 context=3, ignore_whitespace=False):
689 """
690 General differ between branches, bookmarks, revisions of two remote or
691 local but related repositories
692
693 :param org_repo:
694 :param org_ref:
695 :param other_repo:
696 :type other_repo:
697 :type other_ref:
698 """
699
700 org_repo_scm = org_repo.scm_instance
701 other_repo_scm = other_repo.scm_instance
702
703 org_repo = org_repo_scm._repo
704 other_repo = other_repo_scm._repo
705
706 org_ref = safe_str(org_ref[1])
707 other_ref = safe_str(other_ref[1])
708
709 if org_repo_scm == other_repo_scm:
710 log.debug('running diff between %s@%s and %s@%s'
711 % (org_repo.path, org_ref,
712 other_repo.path, other_ref))
713 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
714 ignore_whitespace=ignore_whitespace, context=context)
715 return _diff
716
717 return '' # FIXME: when is it ever relevant to return nothing?
General Comments 0
You need to be logged in to leave comments. Login now