##// END OF EJS Templates
refactoring: drop unused 'discovery data' in pull request and compare diffs
Mads Kiilerich -
r3192:b9105d31 beta
parent child Browse files
Show More
@@ -1,184 +1,183
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 showoing differences between two
6 compare controller for pylons showoing 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.utils2 import str2bool
43 from rhodecode.lib.utils2 import str2bool
44 from rhodecode.lib.diffs import LimitedDiffContainer
44 from rhodecode.lib.diffs import LimitedDiffContainer
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class CompareController(BaseRepoController):
50 class CompareController(BaseRepoController):
51
51
52 @LoginRequired()
52 @LoginRequired()
53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
54 'repository.admin')
54 'repository.admin')
55 def __before__(self):
55 def __before__(self):
56 super(CompareController, self).__before__()
56 super(CompareController, self).__before__()
57
57
58 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
58 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
59 partial=False):
59 partial=False):
60 """
60 """
61 Safe way to get changeset if error occur it redirects to changeset with
61 Safe way to get changeset if error occur it redirects to changeset with
62 proper message. If partial is set then don't do redirect raise Exception
62 proper message. If partial is set then don't do redirect raise Exception
63 instead
63 instead
64
64
65 :param rev: revision to fetch
65 :param rev: revision to fetch
66 :param repo: repo instance
66 :param repo: repo instance
67 """
67 """
68
68
69 try:
69 try:
70 type_, rev = rev
70 type_, rev = rev
71 return repo.scm_instance.get_changeset(rev)
71 return repo.scm_instance.get_changeset(rev)
72 except EmptyRepositoryError, e:
72 except EmptyRepositoryError, e:
73 if not redirect_after:
73 if not redirect_after:
74 return None
74 return None
75 h.flash(h.literal(_('There are no changesets yet')),
75 h.flash(h.literal(_('There are no changesets yet')),
76 category='warning')
76 category='warning')
77 redirect(url('summary_home', repo_name=repo.repo_name))
77 redirect(url('summary_home', repo_name=repo.repo_name))
78
78
79 except RepositoryError, e:
79 except RepositoryError, e:
80 log.error(traceback.format_exc())
80 log.error(traceback.format_exc())
81 h.flash(str(e), category='warning')
81 h.flash(str(e), category='warning')
82 if not partial:
82 if not partial:
83 redirect(h.url('summary_home', repo_name=repo.repo_name))
83 redirect(h.url('summary_home', repo_name=repo.repo_name))
84 raise HTTPBadRequest()
84 raise HTTPBadRequest()
85
85
86 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
86 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
87
87
88 org_repo = c.rhodecode_db_repo.repo_name
88 org_repo = c.rhodecode_db_repo.repo_name
89 org_ref = (org_ref_type, org_ref)
89 org_ref = (org_ref_type, org_ref)
90 other_ref = (other_ref_type, other_ref)
90 other_ref = (other_ref_type, other_ref)
91 other_repo = request.GET.get('repo', org_repo)
91 other_repo = request.GET.get('repo', org_repo)
92 incoming_changesets = str2bool(request.GET.get('bundle', False))
92 incoming_changesets = str2bool(request.GET.get('bundle', False))
93 c.fulldiff = fulldiff = request.GET.get('fulldiff')
93 c.fulldiff = fulldiff = request.GET.get('fulldiff')
94 rev_start = request.GET.get('rev_start')
94 rev_start = request.GET.get('rev_start')
95 rev_end = request.GET.get('rev_end')
95 rev_end = request.GET.get('rev_end')
96
96
97 c.swap_url = h.url('compare_url', repo_name=other_repo,
97 c.swap_url = h.url('compare_url', repo_name=other_repo,
98 org_ref_type=other_ref[0], org_ref=other_ref[1],
98 org_ref_type=other_ref[0], org_ref=other_ref[1],
99 other_ref_type=org_ref[0], other_ref=org_ref[1],
99 other_ref_type=org_ref[0], other_ref=org_ref[1],
100 repo=org_repo, as_form=request.GET.get('as_form'),
100 repo=org_repo, as_form=request.GET.get('as_form'),
101 bundle=incoming_changesets)
101 bundle=incoming_changesets)
102
102
103 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
103 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
104 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
104 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
105
105
106 if c.org_repo is None:
106 if c.org_repo is None:
107 log.error('Could not find org repo %s' % org_repo)
107 log.error('Could not find org repo %s' % org_repo)
108 raise HTTPNotFound
108 raise HTTPNotFound
109 if c.other_repo is None:
109 if c.other_repo is None:
110 log.error('Could not find other repo %s' % other_repo)
110 log.error('Could not find other repo %s' % other_repo)
111 raise HTTPNotFound
111 raise HTTPNotFound
112
112
113 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
113 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
114 log.error('compare of two remote repos not available for GIT REPOS')
114 log.error('compare of two remote repos not available for GIT REPOS')
115 raise HTTPNotFound
115 raise HTTPNotFound
116
116
117 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
117 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
118 log.error('compare of two different kind of remote repos not available')
118 log.error('compare of two different kind of remote repos not available')
119 raise HTTPNotFound
119 raise HTTPNotFound
120
120
121 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
121 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
122 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
122 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
123 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
123 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
124
124
125 if rev_start and rev_end:
125 if rev_start and rev_end:
126 #replace our org_ref with given CS
126 #replace our org_ref with given CS
127 org_ref = ('rev', rev_start)
127 org_ref = ('rev', rev_start)
128 other_ref = ('rev', rev_end)
128 other_ref = ('rev', rev_end)
129
129
130 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
130 c.cs_ranges = PullRequestModel().get_compare_data(
131 org_repo, org_ref, other_repo, other_ref,
131 org_repo, org_ref, other_repo, other_ref,
132 )
132 )
133
133
134 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
134 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
135 c.cs_ranges])
135 c.cs_ranges])
136 c.target_repo = c.repo_name
136 c.target_repo = c.repo_name
137 # defines that we need hidden inputs with changesets
137 # defines that we need hidden inputs with changesets
138 c.as_form = request.GET.get('as_form', False)
138 c.as_form = request.GET.get('as_form', False)
139 if partial:
139 if partial:
140 return render('compare/compare_cs.html')
140 return render('compare/compare_cs.html')
141
141
142 c.org_ref = org_ref[1]
142 c.org_ref = org_ref[1]
143 c.other_ref = other_ref[1]
143 c.other_ref = other_ref[1]
144
144
145 if not incoming_changesets and c.cs_ranges and c.org_repo != c.other_repo:
145 if not incoming_changesets and c.cs_ranges and c.org_repo != c.other_repo:
146 # case we want a simple diff without incoming changesets, just
146 # case we want a simple diff without incoming changesets, just
147 # for review purposes. Make the diff on the forked repo, with
147 # for review purposes. Make the diff on the forked repo, with
148 # revision that is common ancestor
148 # revision that is common ancestor
149 _org_ref = org_ref
149 _org_ref = org_ref
150 org_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
150 org_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
151 if c.cs_ranges[0].parents
151 if c.cs_ranges[0].parents
152 else EmptyChangeset(), 'raw_id'))
152 else EmptyChangeset(), 'raw_id'))
153 log.debug('Changed org_ref from %s to %s' % (_org_ref, org_ref))
153 log.debug('Changed org_ref from %s to %s' % (_org_ref, org_ref))
154 other_repo = org_repo
154 other_repo = org_repo
155
155
156 diff_limit = self.cut_off_limit if not fulldiff else None
156 diff_limit = self.cut_off_limit if not fulldiff else None
157
157
158 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
158 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
159 discovery_data,
160 remote_compare=incoming_changesets)
159 remote_compare=incoming_changesets)
161
160
162 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
161 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
163 diff_limit=diff_limit)
162 diff_limit=diff_limit)
164 _parsed = diff_processor.prepare()
163 _parsed = diff_processor.prepare()
165
164
166 c.limited_diff = False
165 c.limited_diff = False
167 if isinstance(_parsed, LimitedDiffContainer):
166 if isinstance(_parsed, LimitedDiffContainer):
168 c.limited_diff = True
167 c.limited_diff = True
169
168
170 c.files = []
169 c.files = []
171 c.changes = {}
170 c.changes = {}
172 c.lines_added = 0
171 c.lines_added = 0
173 c.lines_deleted = 0
172 c.lines_deleted = 0
174 for f in _parsed:
173 for f in _parsed:
175 st = f['stats']
174 st = f['stats']
176 if st[0] != 'b':
175 if st[0] != 'b':
177 c.lines_added += st[0]
176 c.lines_added += st[0]
178 c.lines_deleted += st[1]
177 c.lines_deleted += st[1]
179 fid = h.FID('', f['filename'])
178 fid = h.FID('', f['filename'])
180 c.files.append([fid, f['operation'], f['filename'], f['stats']])
179 c.files.append([fid, f['operation'], f['filename'], f['stats']])
181 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
180 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
182 c.changes[fid] = [f['operation'], f['filename'], diff]
181 c.changes[fid] = [f['operation'], f['filename'], diff]
183
182
184 return render('compare/compare_diff.html')
183 return render('compare/compare_diff.html')
@@ -1,770 +1,769
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 import traceback
31 import traceback
32
32
33 from itertools import tee, imap
33 from itertools import tee, imap
34
34
35 from mercurial import patch
35 from mercurial import patch
36 from mercurial.mdiff import diffopts
36 from mercurial.mdiff import diffopts
37 from mercurial.bundlerepo import bundlerepository
37 from mercurial.bundlerepo import bundlerepository
38
38
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 from rhodecode.lib.compat import BytesIO
41 from rhodecode.lib.compat import BytesIO
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
43 from rhodecode.lib.vcs.exceptions import VCSError
43 from rhodecode.lib.vcs.exceptions import VCSError
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.helpers import escape
46 from rhodecode.lib.helpers import escape
47 from rhodecode.lib.utils import make_ui
47 from rhodecode.lib.utils import make_ui
48 from rhodecode.lib.utils2 import safe_unicode
48 from rhodecode.lib.utils2 import safe_unicode
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def wrap_to_table(str_):
53 def wrap_to_table(str_):
54 return '''<table class="code-difftable">
54 return '''<table class="code-difftable">
55 <tr class="line no-comment">
55 <tr class="line no-comment">
56 <td class="lineno new"></td>
56 <td class="lineno new"></td>
57 <td class="code no-comment"><pre>%s</pre></td>
57 <td class="code no-comment"><pre>%s</pre></td>
58 </tr>
58 </tr>
59 </table>''' % str_
59 </table>''' % str_
60
60
61
61
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
63 ignore_whitespace=True, line_context=3,
63 ignore_whitespace=True, line_context=3,
64 enable_comments=False):
64 enable_comments=False):
65 """
65 """
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
67 proper message
67 proper message
68 """
68 """
69
69
70 if filenode_old is None:
70 if filenode_old is None:
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
72
72
73 if filenode_old.is_binary or filenode_new.is_binary:
73 if filenode_old.is_binary or filenode_new.is_binary:
74 diff = wrap_to_table(_('binary file'))
74 diff = wrap_to_table(_('binary file'))
75 stats = (0, 0)
75 stats = (0, 0)
76 size = 0
76 size = 0
77
77
78 elif cut_off_limit != -1 and (cut_off_limit is None or
78 elif cut_off_limit != -1 and (cut_off_limit is None or
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
80
80
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
82 ignore_whitespace=ignore_whitespace,
82 ignore_whitespace=ignore_whitespace,
83 context=line_context)
83 context=line_context)
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
85
85
86 diff = diff_processor.as_html(enable_comments=enable_comments)
86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 stats = diff_processor.stat()
87 stats = diff_processor.stat()
88 size = len(diff or '')
88 size = len(diff or '')
89 else:
89 else:
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 'diff menu to display this diff'))
91 'diff menu to display this diff'))
92 stats = (0, 0)
92 stats = (0, 0)
93 size = 0
93 size = 0
94 if not diff:
94 if not diff:
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
96 [filenode_new, filenode_old])
96 [filenode_new, filenode_old])
97 if submodules:
97 if submodules:
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
99 else:
99 else:
100 diff = wrap_to_table(_('No changes detected'))
100 diff = wrap_to_table(_('No changes detected'))
101
101
102 cs1 = filenode_old.changeset.raw_id
102 cs1 = filenode_old.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
104
104
105 return size, cs1, cs2, diff, stats
105 return size, cs1, cs2, diff, stats
106
106
107
107
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
109 """
109 """
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
111
111
112 :param ignore_whitespace: ignore whitespaces in diff
112 :param ignore_whitespace: ignore whitespaces in diff
113 """
113 """
114 # make sure we pass in default context
114 # make sure we pass in default context
115 context = context or 3
115 context = context or 3
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
117 [filenode_new, filenode_old])
117 [filenode_new, filenode_old])
118 if submodules:
118 if submodules:
119 return ''
119 return ''
120
120
121 for filenode in (filenode_old, filenode_new):
121 for filenode in (filenode_old, filenode_new):
122 if not isinstance(filenode, FileNode):
122 if not isinstance(filenode, FileNode):
123 raise VCSError("Given object should be FileNode object, not %s"
123 raise VCSError("Given object should be FileNode object, not %s"
124 % filenode.__class__)
124 % filenode.__class__)
125
125
126 repo = filenode_new.changeset.repository
126 repo = filenode_new.changeset.repository
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
129
129
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
131 ignore_whitespace, context)
131 ignore_whitespace, context)
132 return vcs_gitdiff
132 return vcs_gitdiff
133
133
134 NEW_FILENODE = 1
134 NEW_FILENODE = 1
135 DEL_FILENODE = 2
135 DEL_FILENODE = 2
136 MOD_FILENODE = 3
136 MOD_FILENODE = 3
137 RENAMED_FILENODE = 4
137 RENAMED_FILENODE = 4
138 CHMOD_FILENODE = 5
138 CHMOD_FILENODE = 5
139
139
140
140
141 class DiffLimitExceeded(Exception):
141 class DiffLimitExceeded(Exception):
142 pass
142 pass
143
143
144
144
145 class LimitedDiffContainer(object):
145 class LimitedDiffContainer(object):
146
146
147 def __init__(self, diff_limit, cur_diff_size, diff):
147 def __init__(self, diff_limit, cur_diff_size, diff):
148 self.diff = diff
148 self.diff = diff
149 self.diff_limit = diff_limit
149 self.diff_limit = diff_limit
150 self.cur_diff_size = cur_diff_size
150 self.cur_diff_size = cur_diff_size
151
151
152 def __iter__(self):
152 def __iter__(self):
153 for l in self.diff:
153 for l in self.diff:
154 yield l
154 yield l
155
155
156
156
157 class DiffProcessor(object):
157 class DiffProcessor(object):
158 """
158 """
159 Give it a unified or git diff and it returns a list of the files that were
159 Give it a unified or git diff and it returns a list of the files that were
160 mentioned in the diff together with a dict of meta information that
160 mentioned in the diff together with a dict of meta information that
161 can be used to render it in a HTML template.
161 can be used to render it in a HTML template.
162 """
162 """
163 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
163 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
164 _newline_marker = re.compile(r'^\\ No newline at end of file')
164 _newline_marker = re.compile(r'^\\ No newline at end of file')
165 _git_header_re = re.compile(r"""
165 _git_header_re = re.compile(r"""
166 #^diff[ ]--git
166 #^diff[ ]--git
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
179 """, re.VERBOSE | re.MULTILINE)
179 """, re.VERBOSE | re.MULTILINE)
180 _hg_header_re = re.compile(r"""
180 _hg_header_re = re.compile(r"""
181 #^diff[ ]--git
181 #^diff[ ]--git
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
194 """, re.VERBOSE | re.MULTILINE)
194 """, re.VERBOSE | re.MULTILINE)
195
195
196 #used for inline highlighter word split
196 #used for inline highlighter word split
197 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
197 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
198
198
199 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
199 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
200 """
200 """
201 :param diff: a text in diff format
201 :param diff: a text in diff format
202 :param vcs: type of version controll hg or git
202 :param vcs: type of version controll hg or git
203 :param format: format of diff passed, `udiff` or `gitdiff`
203 :param format: format of diff passed, `udiff` or `gitdiff`
204 :param diff_limit: define the size of diff that is considered "big"
204 :param diff_limit: define the size of diff that is considered "big"
205 based on that parameter cut off will be triggered, set to None
205 based on that parameter cut off will be triggered, set to None
206 to show full diff
206 to show full diff
207 """
207 """
208 if not isinstance(diff, basestring):
208 if not isinstance(diff, basestring):
209 raise Exception('Diff must be a basestring got %s instead' % type(diff))
209 raise Exception('Diff must be a basestring got %s instead' % type(diff))
210
210
211 self._diff = diff
211 self._diff = diff
212 self._format = format
212 self._format = format
213 self.adds = 0
213 self.adds = 0
214 self.removes = 0
214 self.removes = 0
215 # calculate diff size
215 # calculate diff size
216 self.diff_size = len(diff)
216 self.diff_size = len(diff)
217 self.diff_limit = diff_limit
217 self.diff_limit = diff_limit
218 self.cur_diff_size = 0
218 self.cur_diff_size = 0
219 self.parsed = False
219 self.parsed = False
220 self.parsed_diff = []
220 self.parsed_diff = []
221 self.vcs = vcs
221 self.vcs = vcs
222
222
223 if format == 'gitdiff':
223 if format == 'gitdiff':
224 self.differ = self._highlight_line_difflib
224 self.differ = self._highlight_line_difflib
225 self._parser = self._parse_gitdiff
225 self._parser = self._parse_gitdiff
226 else:
226 else:
227 self.differ = self._highlight_line_udiff
227 self.differ = self._highlight_line_udiff
228 self._parser = self._parse_udiff
228 self._parser = self._parse_udiff
229
229
230 def _copy_iterator(self):
230 def _copy_iterator(self):
231 """
231 """
232 make a fresh copy of generator, we should not iterate thru
232 make a fresh copy of generator, we should not iterate thru
233 an original as it's needed for repeating operations on
233 an original as it's needed for repeating operations on
234 this instance of DiffProcessor
234 this instance of DiffProcessor
235 """
235 """
236 self.__udiff, iterator_copy = tee(self.__udiff)
236 self.__udiff, iterator_copy = tee(self.__udiff)
237 return iterator_copy
237 return iterator_copy
238
238
239 def _escaper(self, string):
239 def _escaper(self, string):
240 """
240 """
241 Escaper for diff escapes special chars and checks the diff limit
241 Escaper for diff escapes special chars and checks the diff limit
242
242
243 :param string:
243 :param string:
244 :type string:
244 :type string:
245 """
245 """
246
246
247 self.cur_diff_size += len(string)
247 self.cur_diff_size += len(string)
248
248
249 # escaper get's iterated on each .next() call and it checks if each
249 # escaper get's iterated on each .next() call and it checks if each
250 # parsed line doesn't exceed the diff limit
250 # parsed line doesn't exceed the diff limit
251 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
251 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
252 raise DiffLimitExceeded('Diff Limit Exceeded')
252 raise DiffLimitExceeded('Diff Limit Exceeded')
253
253
254 return safe_unicode(string).replace('&', '&amp;')\
254 return safe_unicode(string).replace('&', '&amp;')\
255 .replace('<', '&lt;')\
255 .replace('<', '&lt;')\
256 .replace('>', '&gt;')
256 .replace('>', '&gt;')
257
257
258 def _line_counter(self, l):
258 def _line_counter(self, l):
259 """
259 """
260 Checks each line and bumps total adds/removes for this diff
260 Checks each line and bumps total adds/removes for this diff
261
261
262 :param l:
262 :param l:
263 """
263 """
264 if l.startswith('+') and not l.startswith('+++'):
264 if l.startswith('+') and not l.startswith('+++'):
265 self.adds += 1
265 self.adds += 1
266 elif l.startswith('-') and not l.startswith('---'):
266 elif l.startswith('-') and not l.startswith('---'):
267 self.removes += 1
267 self.removes += 1
268 return safe_unicode(l)
268 return safe_unicode(l)
269
269
270 def _highlight_line_difflib(self, line, next_):
270 def _highlight_line_difflib(self, line, next_):
271 """
271 """
272 Highlight inline changes in both lines.
272 Highlight inline changes in both lines.
273 """
273 """
274
274
275 if line['action'] == 'del':
275 if line['action'] == 'del':
276 old, new = line, next_
276 old, new = line, next_
277 else:
277 else:
278 old, new = next_, line
278 old, new = next_, line
279
279
280 oldwords = self._token_re.split(old['line'])
280 oldwords = self._token_re.split(old['line'])
281 newwords = self._token_re.split(new['line'])
281 newwords = self._token_re.split(new['line'])
282 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
282 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
283
283
284 oldfragments, newfragments = [], []
284 oldfragments, newfragments = [], []
285 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
285 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
286 oldfrag = ''.join(oldwords[i1:i2])
286 oldfrag = ''.join(oldwords[i1:i2])
287 newfrag = ''.join(newwords[j1:j2])
287 newfrag = ''.join(newwords[j1:j2])
288 if tag != 'equal':
288 if tag != 'equal':
289 if oldfrag:
289 if oldfrag:
290 oldfrag = '<del>%s</del>' % oldfrag
290 oldfrag = '<del>%s</del>' % oldfrag
291 if newfrag:
291 if newfrag:
292 newfrag = '<ins>%s</ins>' % newfrag
292 newfrag = '<ins>%s</ins>' % newfrag
293 oldfragments.append(oldfrag)
293 oldfragments.append(oldfrag)
294 newfragments.append(newfrag)
294 newfragments.append(newfrag)
295
295
296 old['line'] = "".join(oldfragments)
296 old['line'] = "".join(oldfragments)
297 new['line'] = "".join(newfragments)
297 new['line'] = "".join(newfragments)
298
298
299 def _highlight_line_udiff(self, line, next_):
299 def _highlight_line_udiff(self, line, next_):
300 """
300 """
301 Highlight inline changes in both lines.
301 Highlight inline changes in both lines.
302 """
302 """
303 start = 0
303 start = 0
304 limit = min(len(line['line']), len(next_['line']))
304 limit = min(len(line['line']), len(next_['line']))
305 while start < limit and line['line'][start] == next_['line'][start]:
305 while start < limit and line['line'][start] == next_['line'][start]:
306 start += 1
306 start += 1
307 end = -1
307 end = -1
308 limit -= start
308 limit -= start
309 while -end <= limit and line['line'][end] == next_['line'][end]:
309 while -end <= limit and line['line'][end] == next_['line'][end]:
310 end -= 1
310 end -= 1
311 end += 1
311 end += 1
312 if start or end:
312 if start or end:
313 def do(l):
313 def do(l):
314 last = end + len(l['line'])
314 last = end + len(l['line'])
315 if l['action'] == 'add':
315 if l['action'] == 'add':
316 tag = 'ins'
316 tag = 'ins'
317 else:
317 else:
318 tag = 'del'
318 tag = 'del'
319 l['line'] = '%s<%s>%s</%s>%s' % (
319 l['line'] = '%s<%s>%s</%s>%s' % (
320 l['line'][:start],
320 l['line'][:start],
321 tag,
321 tag,
322 l['line'][start:last],
322 l['line'][start:last],
323 tag,
323 tag,
324 l['line'][last:]
324 l['line'][last:]
325 )
325 )
326 do(line)
326 do(line)
327 do(next_)
327 do(next_)
328
328
329 def _get_header(self, diff_chunk):
329 def _get_header(self, diff_chunk):
330 """
330 """
331 parses the diff header, and returns parts, and leftover diff
331 parses the diff header, and returns parts, and leftover diff
332 parts consists of 14 elements::
332 parts consists of 14 elements::
333
333
334 a_path, b_path, similarity_index, rename_from, rename_to,
334 a_path, b_path, similarity_index, rename_from, rename_to,
335 old_mode, new_mode, new_file_mode, deleted_file_mode,
335 old_mode, new_mode, new_file_mode, deleted_file_mode,
336 a_blob_id, b_blob_id, b_mode, a_file, b_file
336 a_blob_id, b_blob_id, b_mode, a_file, b_file
337
337
338 :param diff_chunk:
338 :param diff_chunk:
339 :type diff_chunk:
339 :type diff_chunk:
340 """
340 """
341
341
342 if self.vcs == 'git':
342 if self.vcs == 'git':
343 match = self._git_header_re.match(diff_chunk)
343 match = self._git_header_re.match(diff_chunk)
344 diff = diff_chunk[match.end():]
344 diff = diff_chunk[match.end():]
345 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
345 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
346 elif self.vcs == 'hg':
346 elif self.vcs == 'hg':
347 match = self._hg_header_re.match(diff_chunk)
347 match = self._hg_header_re.match(diff_chunk)
348 diff = diff_chunk[match.end():]
348 diff = diff_chunk[match.end():]
349 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
349 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
350 else:
350 else:
351 raise Exception('VCS type %s is not supported' % self.vcs)
351 raise Exception('VCS type %s is not supported' % self.vcs)
352
352
353 def _clean_line(self, line, command):
353 def _clean_line(self, line, command):
354 if command in ['+', '-', ' ']:
354 if command in ['+', '-', ' ']:
355 #only modify the line if it's actually a diff thing
355 #only modify the line if it's actually a diff thing
356 line = line[1:]
356 line = line[1:]
357 return line
357 return line
358
358
359 def _parse_gitdiff(self, inline_diff=True):
359 def _parse_gitdiff(self, inline_diff=True):
360 _files = []
360 _files = []
361 diff_container = lambda arg: arg
361 diff_container = lambda arg: arg
362
362
363 ##split the diff in chunks of separate --git a/file b/file chunks
363 ##split the diff in chunks of separate --git a/file b/file chunks
364 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
364 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
365 binary = False
365 binary = False
366 binary_msg = 'unknown binary'
366 binary_msg = 'unknown binary'
367 head, diff = self._get_header(raw_diff)
367 head, diff = self._get_header(raw_diff)
368
368
369 if not head['a_file'] and head['b_file']:
369 if not head['a_file'] and head['b_file']:
370 op = 'A'
370 op = 'A'
371 elif head['a_file'] and head['b_file']:
371 elif head['a_file'] and head['b_file']:
372 op = 'M'
372 op = 'M'
373 elif head['a_file'] and not head['b_file']:
373 elif head['a_file'] and not head['b_file']:
374 op = 'D'
374 op = 'D'
375 else:
375 else:
376 #probably we're dealing with a binary file 1
376 #probably we're dealing with a binary file 1
377 binary = True
377 binary = True
378 if head['deleted_file_mode']:
378 if head['deleted_file_mode']:
379 op = 'D'
379 op = 'D'
380 stats = ['b', DEL_FILENODE]
380 stats = ['b', DEL_FILENODE]
381 binary_msg = 'deleted binary file'
381 binary_msg = 'deleted binary file'
382 elif head['new_file_mode']:
382 elif head['new_file_mode']:
383 op = 'A'
383 op = 'A'
384 stats = ['b', NEW_FILENODE]
384 stats = ['b', NEW_FILENODE]
385 binary_msg = 'new binary file %s' % head['new_file_mode']
385 binary_msg = 'new binary file %s' % head['new_file_mode']
386 else:
386 else:
387 if head['new_mode'] and head['old_mode']:
387 if head['new_mode'] and head['old_mode']:
388 stats = ['b', CHMOD_FILENODE]
388 stats = ['b', CHMOD_FILENODE]
389 op = 'M'
389 op = 'M'
390 binary_msg = ('modified binary file chmod %s => %s'
390 binary_msg = ('modified binary file chmod %s => %s'
391 % (head['old_mode'], head['new_mode']))
391 % (head['old_mode'], head['new_mode']))
392 elif (head['rename_from'] and head['rename_to']
392 elif (head['rename_from'] and head['rename_to']
393 and head['rename_from'] != head['rename_to']):
393 and head['rename_from'] != head['rename_to']):
394 stats = ['b', RENAMED_FILENODE]
394 stats = ['b', RENAMED_FILENODE]
395 op = 'M'
395 op = 'M'
396 binary_msg = ('file renamed from %s to %s'
396 binary_msg = ('file renamed from %s to %s'
397 % (head['rename_from'], head['rename_to']))
397 % (head['rename_from'], head['rename_to']))
398 else:
398 else:
399 stats = ['b', MOD_FILENODE]
399 stats = ['b', MOD_FILENODE]
400 op = 'M'
400 op = 'M'
401 binary_msg = 'modified binary file'
401 binary_msg = 'modified binary file'
402
402
403 if not binary:
403 if not binary:
404 try:
404 try:
405 chunks, stats = self._parse_lines(diff)
405 chunks, stats = self._parse_lines(diff)
406 except DiffLimitExceeded:
406 except DiffLimitExceeded:
407 diff_container = lambda _diff: LimitedDiffContainer(
407 diff_container = lambda _diff: LimitedDiffContainer(
408 self.diff_limit,
408 self.diff_limit,
409 self.cur_diff_size,
409 self.cur_diff_size,
410 _diff)
410 _diff)
411 break
411 break
412 else:
412 else:
413 chunks = []
413 chunks = []
414 chunks.append([{
414 chunks.append([{
415 'old_lineno': '',
415 'old_lineno': '',
416 'new_lineno': '',
416 'new_lineno': '',
417 'action': 'binary',
417 'action': 'binary',
418 'line': binary_msg,
418 'line': binary_msg,
419 }])
419 }])
420
420
421 _files.append({
421 _files.append({
422 'filename': head['b_path'],
422 'filename': head['b_path'],
423 'old_revision': head['a_blob_id'],
423 'old_revision': head['a_blob_id'],
424 'new_revision': head['b_blob_id'],
424 'new_revision': head['b_blob_id'],
425 'chunks': chunks,
425 'chunks': chunks,
426 'operation': op,
426 'operation': op,
427 'stats': stats,
427 'stats': stats,
428 })
428 })
429
429
430 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
430 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
431
431
432 if inline_diff is False:
432 if inline_diff is False:
433 return diff_container(sorted(_files, key=sorter))
433 return diff_container(sorted(_files, key=sorter))
434
434
435 # highlight inline changes
435 # highlight inline changes
436 for diff_data in _files:
436 for diff_data in _files:
437 for chunk in diff_data['chunks']:
437 for chunk in diff_data['chunks']:
438 lineiter = iter(chunk)
438 lineiter = iter(chunk)
439 try:
439 try:
440 while 1:
440 while 1:
441 line = lineiter.next()
441 line = lineiter.next()
442 if line['action'] not in ['unmod', 'context']:
442 if line['action'] not in ['unmod', 'context']:
443 nextline = lineiter.next()
443 nextline = lineiter.next()
444 if nextline['action'] in ['unmod', 'context'] or \
444 if nextline['action'] in ['unmod', 'context'] or \
445 nextline['action'] == line['action']:
445 nextline['action'] == line['action']:
446 continue
446 continue
447 self.differ(line, nextline)
447 self.differ(line, nextline)
448 except StopIteration:
448 except StopIteration:
449 pass
449 pass
450
450
451 return diff_container(sorted(_files, key=sorter))
451 return diff_container(sorted(_files, key=sorter))
452
452
453 def _parse_udiff(self, inline_diff=True):
453 def _parse_udiff(self, inline_diff=True):
454 raise NotImplementedError()
454 raise NotImplementedError()
455
455
456 def _parse_lines(self, diff):
456 def _parse_lines(self, diff):
457 """
457 """
458 Parse the diff an return data for the template.
458 Parse the diff an return data for the template.
459 """
459 """
460
460
461 lineiter = iter(diff)
461 lineiter = iter(diff)
462 stats = [0, 0]
462 stats = [0, 0]
463
463
464 try:
464 try:
465 chunks = []
465 chunks = []
466 line = lineiter.next()
466 line = lineiter.next()
467
467
468 while line:
468 while line:
469 lines = []
469 lines = []
470 chunks.append(lines)
470 chunks.append(lines)
471
471
472 match = self._chunk_re.match(line)
472 match = self._chunk_re.match(line)
473
473
474 if not match:
474 if not match:
475 break
475 break
476
476
477 gr = match.groups()
477 gr = match.groups()
478 (old_line, old_end,
478 (old_line, old_end,
479 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
479 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
480 old_line -= 1
480 old_line -= 1
481 new_line -= 1
481 new_line -= 1
482
482
483 context = len(gr) == 5
483 context = len(gr) == 5
484 old_end += old_line
484 old_end += old_line
485 new_end += new_line
485 new_end += new_line
486
486
487 if context:
487 if context:
488 # skip context only if it's first line
488 # skip context only if it's first line
489 if int(gr[0]) > 1:
489 if int(gr[0]) > 1:
490 lines.append({
490 lines.append({
491 'old_lineno': '...',
491 'old_lineno': '...',
492 'new_lineno': '...',
492 'new_lineno': '...',
493 'action': 'context',
493 'action': 'context',
494 'line': line,
494 'line': line,
495 })
495 })
496
496
497 line = lineiter.next()
497 line = lineiter.next()
498
498
499 while old_line < old_end or new_line < new_end:
499 while old_line < old_end or new_line < new_end:
500 command = ' '
500 command = ' '
501 if line:
501 if line:
502 command = line[0]
502 command = line[0]
503
503
504 affects_old = affects_new = False
504 affects_old = affects_new = False
505
505
506 # ignore those if we don't expect them
506 # ignore those if we don't expect them
507 if command in '#@':
507 if command in '#@':
508 continue
508 continue
509 elif command == '+':
509 elif command == '+':
510 affects_new = True
510 affects_new = True
511 action = 'add'
511 action = 'add'
512 stats[0] += 1
512 stats[0] += 1
513 elif command == '-':
513 elif command == '-':
514 affects_old = True
514 affects_old = True
515 action = 'del'
515 action = 'del'
516 stats[1] += 1
516 stats[1] += 1
517 else:
517 else:
518 affects_old = affects_new = True
518 affects_old = affects_new = True
519 action = 'unmod'
519 action = 'unmod'
520
520
521 if not self._newline_marker.match(line):
521 if not self._newline_marker.match(line):
522 old_line += affects_old
522 old_line += affects_old
523 new_line += affects_new
523 new_line += affects_new
524 lines.append({
524 lines.append({
525 'old_lineno': affects_old and old_line or '',
525 'old_lineno': affects_old and old_line or '',
526 'new_lineno': affects_new and new_line or '',
526 'new_lineno': affects_new and new_line or '',
527 'action': action,
527 'action': action,
528 'line': self._clean_line(line, command)
528 'line': self._clean_line(line, command)
529 })
529 })
530
530
531 line = lineiter.next()
531 line = lineiter.next()
532
532
533 if self._newline_marker.match(line):
533 if self._newline_marker.match(line):
534 # we need to append to lines, since this is not
534 # we need to append to lines, since this is not
535 # counted in the line specs of diff
535 # counted in the line specs of diff
536 lines.append({
536 lines.append({
537 'old_lineno': '...',
537 'old_lineno': '...',
538 'new_lineno': '...',
538 'new_lineno': '...',
539 'action': 'context',
539 'action': 'context',
540 'line': self._clean_line(line, command)
540 'line': self._clean_line(line, command)
541 })
541 })
542
542
543 except StopIteration:
543 except StopIteration:
544 pass
544 pass
545 return chunks, stats
545 return chunks, stats
546
546
547 def _safe_id(self, idstring):
547 def _safe_id(self, idstring):
548 """Make a string safe for including in an id attribute.
548 """Make a string safe for including in an id attribute.
549
549
550 The HTML spec says that id attributes 'must begin with
550 The HTML spec says that id attributes 'must begin with
551 a letter ([A-Za-z]) and may be followed by any number
551 a letter ([A-Za-z]) and may be followed by any number
552 of letters, digits ([0-9]), hyphens ("-"), underscores
552 of letters, digits ([0-9]), hyphens ("-"), underscores
553 ("_"), colons (":"), and periods (".")'. These regexps
553 ("_"), colons (":"), and periods (".")'. These regexps
554 are slightly over-zealous, in that they remove colons
554 are slightly over-zealous, in that they remove colons
555 and periods unnecessarily.
555 and periods unnecessarily.
556
556
557 Whitespace is transformed into underscores, and then
557 Whitespace is transformed into underscores, and then
558 anything which is not a hyphen or a character that
558 anything which is not a hyphen or a character that
559 matches \w (alphanumerics and underscore) is removed.
559 matches \w (alphanumerics and underscore) is removed.
560
560
561 """
561 """
562 # Transform all whitespace to underscore
562 # Transform all whitespace to underscore
563 idstring = re.sub(r'\s', "_", '%s' % idstring)
563 idstring = re.sub(r'\s', "_", '%s' % idstring)
564 # Remove everything that is not a hyphen or a member of \w
564 # Remove everything that is not a hyphen or a member of \w
565 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
565 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
566 return idstring
566 return idstring
567
567
568 def prepare(self, inline_diff=True):
568 def prepare(self, inline_diff=True):
569 """
569 """
570 Prepare the passed udiff for HTML rendering. It'l return a list
570 Prepare the passed udiff for HTML rendering. It'l return a list
571 of dicts with diff information
571 of dicts with diff information
572 """
572 """
573 parsed = self._parser(inline_diff=inline_diff)
573 parsed = self._parser(inline_diff=inline_diff)
574 self.parsed = True
574 self.parsed = True
575 self.parsed_diff = parsed
575 self.parsed_diff = parsed
576 return parsed
576 return parsed
577
577
578 def as_raw(self, diff_lines=None):
578 def as_raw(self, diff_lines=None):
579 """
579 """
580 Returns raw string diff
580 Returns raw string diff
581 """
581 """
582 return self._diff
582 return self._diff
583 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
583 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
584
584
585 def as_html(self, table_class='code-difftable', line_class='line',
585 def as_html(self, table_class='code-difftable', line_class='line',
586 old_lineno_class='lineno old', new_lineno_class='lineno new',
586 old_lineno_class='lineno old', new_lineno_class='lineno new',
587 code_class='code', enable_comments=False, parsed_lines=None):
587 code_class='code', enable_comments=False, parsed_lines=None):
588 """
588 """
589 Return given diff as html table with customized css classes
589 Return given diff as html table with customized css classes
590 """
590 """
591 def _link_to_if(condition, label, url):
591 def _link_to_if(condition, label, url):
592 """
592 """
593 Generates a link if condition is meet or just the label if not.
593 Generates a link if condition is meet or just the label if not.
594 """
594 """
595
595
596 if condition:
596 if condition:
597 return '''<a href="%(url)s">%(label)s</a>''' % {
597 return '''<a href="%(url)s">%(label)s</a>''' % {
598 'url': url,
598 'url': url,
599 'label': label
599 'label': label
600 }
600 }
601 else:
601 else:
602 return label
602 return label
603 if not self.parsed:
603 if not self.parsed:
604 self.prepare()
604 self.prepare()
605
605
606 diff_lines = self.parsed_diff
606 diff_lines = self.parsed_diff
607 if parsed_lines:
607 if parsed_lines:
608 diff_lines = parsed_lines
608 diff_lines = parsed_lines
609
609
610 _html_empty = True
610 _html_empty = True
611 _html = []
611 _html = []
612 _html.append('''<table class="%(table_class)s">\n''' % {
612 _html.append('''<table class="%(table_class)s">\n''' % {
613 'table_class': table_class
613 'table_class': table_class
614 })
614 })
615
615
616 for diff in diff_lines:
616 for diff in diff_lines:
617 for line in diff['chunks']:
617 for line in diff['chunks']:
618 _html_empty = False
618 _html_empty = False
619 for change in line:
619 for change in line:
620 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
620 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
621 'lc': line_class,
621 'lc': line_class,
622 'action': change['action']
622 'action': change['action']
623 })
623 })
624 anchor_old_id = ''
624 anchor_old_id = ''
625 anchor_new_id = ''
625 anchor_new_id = ''
626 anchor_old = "%(filename)s_o%(oldline_no)s" % {
626 anchor_old = "%(filename)s_o%(oldline_no)s" % {
627 'filename': self._safe_id(diff['filename']),
627 'filename': self._safe_id(diff['filename']),
628 'oldline_no': change['old_lineno']
628 'oldline_no': change['old_lineno']
629 }
629 }
630 anchor_new = "%(filename)s_n%(oldline_no)s" % {
630 anchor_new = "%(filename)s_n%(oldline_no)s" % {
631 'filename': self._safe_id(diff['filename']),
631 'filename': self._safe_id(diff['filename']),
632 'oldline_no': change['new_lineno']
632 'oldline_no': change['new_lineno']
633 }
633 }
634 cond_old = (change['old_lineno'] != '...' and
634 cond_old = (change['old_lineno'] != '...' and
635 change['old_lineno'])
635 change['old_lineno'])
636 cond_new = (change['new_lineno'] != '...' and
636 cond_new = (change['new_lineno'] != '...' and
637 change['new_lineno'])
637 change['new_lineno'])
638 if cond_old:
638 if cond_old:
639 anchor_old_id = 'id="%s"' % anchor_old
639 anchor_old_id = 'id="%s"' % anchor_old
640 if cond_new:
640 if cond_new:
641 anchor_new_id = 'id="%s"' % anchor_new
641 anchor_new_id = 'id="%s"' % anchor_new
642 ###########################################################
642 ###########################################################
643 # OLD LINE NUMBER
643 # OLD LINE NUMBER
644 ###########################################################
644 ###########################################################
645 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
645 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
646 'a_id': anchor_old_id,
646 'a_id': anchor_old_id,
647 'olc': old_lineno_class
647 'olc': old_lineno_class
648 })
648 })
649
649
650 _html.append('''%(link)s''' % {
650 _html.append('''%(link)s''' % {
651 'link': _link_to_if(True, change['old_lineno'],
651 'link': _link_to_if(True, change['old_lineno'],
652 '#%s' % anchor_old)
652 '#%s' % anchor_old)
653 })
653 })
654 _html.append('''</td>\n''')
654 _html.append('''</td>\n''')
655 ###########################################################
655 ###########################################################
656 # NEW LINE NUMBER
656 # NEW LINE NUMBER
657 ###########################################################
657 ###########################################################
658
658
659 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
659 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
660 'a_id': anchor_new_id,
660 'a_id': anchor_new_id,
661 'nlc': new_lineno_class
661 'nlc': new_lineno_class
662 })
662 })
663
663
664 _html.append('''%(link)s''' % {
664 _html.append('''%(link)s''' % {
665 'link': _link_to_if(True, change['new_lineno'],
665 'link': _link_to_if(True, change['new_lineno'],
666 '#%s' % anchor_new)
666 '#%s' % anchor_new)
667 })
667 })
668 _html.append('''</td>\n''')
668 _html.append('''</td>\n''')
669 ###########################################################
669 ###########################################################
670 # CODE
670 # CODE
671 ###########################################################
671 ###########################################################
672 comments = '' if enable_comments else 'no-comment'
672 comments = '' if enable_comments else 'no-comment'
673 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
673 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
674 'cc': code_class,
674 'cc': code_class,
675 'inc': comments
675 'inc': comments
676 })
676 })
677 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
677 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
678 'code': change['line']
678 'code': change['line']
679 })
679 })
680
680
681 _html.append('''\t</td>''')
681 _html.append('''\t</td>''')
682 _html.append('''\n</tr>\n''')
682 _html.append('''\n</tr>\n''')
683 _html.append('''</table>''')
683 _html.append('''</table>''')
684 if _html_empty:
684 if _html_empty:
685 return None
685 return None
686 return ''.join(_html)
686 return ''.join(_html)
687
687
688 def stat(self):
688 def stat(self):
689 """
689 """
690 Returns tuple of added, and removed lines for this instance
690 Returns tuple of added, and removed lines for this instance
691 """
691 """
692 return self.adds, self.removes
692 return self.adds, self.removes
693
693
694
694
695 class InMemoryBundleRepo(bundlerepository):
695 class InMemoryBundleRepo(bundlerepository):
696 def __init__(self, ui, path, bundlestream):
696 def __init__(self, ui, path, bundlestream):
697 self._tempparent = None
697 self._tempparent = None
698 localrepo.localrepository.__init__(self, ui, path)
698 localrepo.localrepository.__init__(self, ui, path)
699 self.ui.setconfig('phases', 'publish', False)
699 self.ui.setconfig('phases', 'publish', False)
700
700
701 self.bundle = bundlestream
701 self.bundle = bundlestream
702
702
703 # dict with the mapping 'filename' -> position in the bundle
703 # dict with the mapping 'filename' -> position in the bundle
704 self.bundlefilespos = {}
704 self.bundlefilespos = {}
705
705
706
706
707 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
707 def differ(org_repo, org_ref, other_repo, other_ref,
708 remote_compare=False, context=3, ignore_whitespace=False):
708 remote_compare=False, context=3, ignore_whitespace=False):
709 """
709 """
710 General differ between branches, bookmarks, revisions of two remote or
710 General differ between branches, bookmarks, revisions of two remote or
711 local but related repositories
711 local but related repositories
712
712
713 :param org_repo:
713 :param org_repo:
714 :param org_ref:
714 :param org_ref:
715 :param other_repo:
715 :param other_repo:
716 :type other_repo:
716 :type other_repo:
717 :type other_ref:
717 :type other_ref:
718 """
718 """
719
719
720 org_repo_scm = org_repo.scm_instance
720 org_repo_scm = org_repo.scm_instance
721 other_repo_scm = other_repo.scm_instance
721 other_repo_scm = other_repo.scm_instance
722
722
723 org_repo = org_repo_scm._repo
723 org_repo = org_repo_scm._repo
724 other_repo = other_repo_scm._repo
724 other_repo = other_repo_scm._repo
725
725
726 org_ref = org_ref[1]
726 org_ref = org_ref[1]
727 other_ref = other_ref[1]
727 other_ref = other_ref[1]
728
728
729 if org_repo_scm == other_repo_scm:
729 if org_repo_scm == other_repo_scm:
730 log.debug('running diff between %s@%s and %s@%s'
730 log.debug('running diff between %s@%s and %s@%s'
731 % (org_repo.path, org_ref, other_repo.path, other_ref))
731 % (org_repo.path, org_ref, other_repo.path, other_ref))
732 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
732 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
733 ignore_whitespace=ignore_whitespace, context=context)
733 ignore_whitespace=ignore_whitespace, context=context)
734 return _diff
734 return _diff
735
735
736 elif remote_compare:
736 elif remote_compare:
737 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
737 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
738 common, incoming, rheads = discovery_data
739 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
738 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
740 # create a bundle (uncompressed if other repo is not local)
739 # create a bundle (uncompressed if other repo is not local)
741 if org_repo_peer.capable('getbundle'):
740 if org_repo_peer.capable('getbundle'):
742 # disable repo hooks here since it's just bundle !
741 # disable repo hooks here since it's just bundle !
743 # patch and reset hooks section of UI config to not run any
742 # patch and reset hooks section of UI config to not run any
744 # hooks on fetching archives with subrepos
743 # hooks on fetching archives with subrepos
745 for k, _ in org_repo.ui.configitems('hooks'):
744 for k, _ in org_repo.ui.configitems('hooks'):
746 org_repo.ui.setconfig('hooks', k, None)
745 org_repo.ui.setconfig('hooks', k, None)
747 unbundle = org_repo.getbundle('incoming', common=None,
746 unbundle = org_repo.getbundle('incoming', common=None,
748 heads=None)
747 heads=None)
749
748
750 buf = BytesIO()
749 buf = BytesIO()
751 while True:
750 while True:
752 chunk = unbundle._stream.read(1024 * 4)
751 chunk = unbundle._stream.read(1024 * 4)
753 if not chunk:
752 if not chunk:
754 break
753 break
755 buf.write(chunk)
754 buf.write(chunk)
756
755
757 buf.seek(0)
756 buf.seek(0)
758 # replace chunked _stream with data that can do tell() and seek()
757 # replace chunked _stream with data that can do tell() and seek()
759 unbundle._stream = buf
758 unbundle._stream = buf
760
759
761 ui = make_ui('db')
760 ui = make_ui('db')
762 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
761 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
763 bundlestream=unbundle)
762 bundlestream=unbundle)
764
763
765 return ''.join(patch.diff(bundlerepo,
764 return ''.join(patch.diff(bundlerepo,
766 node1=other_repo[other_ref].node(),
765 node1=other_repo[other_ref].node(),
767 node2=org_repo[org_ref].node(),
766 node2=org_repo[org_ref].node(),
768 opts=opts))
767 opts=opts))
769
768
770 return ''
769 return ''
@@ -1,295 +1,251
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.pull_request
3 rhodecode.model.pull_request
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull request model for RhodeCode
6 pull request model for RhodeCode
7
7
8 :created_on: Jun 6, 2012
8 :created_on: Jun 6, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2012-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
25
26 import logging
26 import logging
27 import binascii
27 import binascii
28 import datetime
28 import datetime
29 import re
29 import re
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\
36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\
37 ChangesetStatus
37 ChangesetStatus
38 from rhodecode.model.notification import NotificationModel
38 from rhodecode.model.notification import NotificationModel
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40
40
41 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
41 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
42 findcommonoutgoing
42 findcommonoutgoing
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class PullRequestModel(BaseModel):
47 class PullRequestModel(BaseModel):
48
48
49 cls = PullRequest
49 cls = PullRequest
50
50
51 def __get_pull_request(self, pull_request):
51 def __get_pull_request(self, pull_request):
52 return self._get_instance(PullRequest, pull_request)
52 return self._get_instance(PullRequest, pull_request)
53
53
54 def get_all(self, repo):
54 def get_all(self, repo):
55 repo = self._get_repo(repo)
55 repo = self._get_repo(repo)
56 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
56 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
57
57
58 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
58 def create(self, created_by, org_repo, org_ref, other_repo, other_ref,
59 revisions, reviewers, title, description=None):
59 revisions, reviewers, title, description=None):
60 from rhodecode.model.changeset_status import ChangesetStatusModel
60 from rhodecode.model.changeset_status import ChangesetStatusModel
61
61
62 created_by_user = self._get_user(created_by)
62 created_by_user = self._get_user(created_by)
63 org_repo = self._get_repo(org_repo)
63 org_repo = self._get_repo(org_repo)
64 other_repo = self._get_repo(other_repo)
64 other_repo = self._get_repo(other_repo)
65
65
66 new = PullRequest()
66 new = PullRequest()
67 new.org_repo = org_repo
67 new.org_repo = org_repo
68 new.org_ref = org_ref
68 new.org_ref = org_ref
69 new.other_repo = other_repo
69 new.other_repo = other_repo
70 new.other_ref = other_ref
70 new.other_ref = other_ref
71 new.revisions = revisions
71 new.revisions = revisions
72 new.title = title
72 new.title = title
73 new.description = description
73 new.description = description
74 new.author = created_by_user
74 new.author = created_by_user
75 self.sa.add(new)
75 self.sa.add(new)
76 Session().flush()
76 Session().flush()
77 #members
77 #members
78 for member in reviewers:
78 for member in reviewers:
79 _usr = self._get_user(member)
79 _usr = self._get_user(member)
80 reviewer = PullRequestReviewers(_usr, new)
80 reviewer = PullRequestReviewers(_usr, new)
81 self.sa.add(reviewer)
81 self.sa.add(reviewer)
82
82
83 #reset state to under-review
83 #reset state to under-review
84 ChangesetStatusModel().set_status(
84 ChangesetStatusModel().set_status(
85 repo=org_repo,
85 repo=org_repo,
86 status=ChangesetStatus.STATUS_UNDER_REVIEW,
86 status=ChangesetStatus.STATUS_UNDER_REVIEW,
87 user=created_by_user,
87 user=created_by_user,
88 pull_request=new
88 pull_request=new
89 )
89 )
90
90
91 #notification to reviewers
91 #notification to reviewers
92 notif = NotificationModel()
92 notif = NotificationModel()
93
93
94 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
94 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
95 pull_request_id=new.pull_request_id,
95 pull_request_id=new.pull_request_id,
96 qualified=True,
96 qualified=True,
97 )
97 )
98 subject = safe_unicode(
98 subject = safe_unicode(
99 h.link_to(
99 h.link_to(
100 _('%(user)s wants you to review pull request #%(pr_id)s') % \
100 _('%(user)s wants you to review pull request #%(pr_id)s') % \
101 {'user': created_by_user.username,
101 {'user': created_by_user.username,
102 'pr_id': new.pull_request_id},
102 'pr_id': new.pull_request_id},
103 pr_url
103 pr_url
104 )
104 )
105 )
105 )
106 body = description
106 body = description
107 kwargs = {
107 kwargs = {
108 'pr_title': title,
108 'pr_title': title,
109 'pr_user_created': h.person(created_by_user.email),
109 'pr_user_created': h.person(created_by_user.email),
110 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
110 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
111 qualified=True,),
111 qualified=True,),
112 'pr_url': pr_url,
112 'pr_url': pr_url,
113 'pr_revisions': revisions
113 'pr_revisions': revisions
114 }
114 }
115 notif.create(created_by=created_by_user, subject=subject, body=body,
115 notif.create(created_by=created_by_user, subject=subject, body=body,
116 recipients=reviewers,
116 recipients=reviewers,
117 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
117 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
118 return new
118 return new
119
119
120 def update_reviewers(self, pull_request, reviewers_ids):
120 def update_reviewers(self, pull_request, reviewers_ids):
121 reviewers_ids = set(reviewers_ids)
121 reviewers_ids = set(reviewers_ids)
122 pull_request = self.__get_pull_request(pull_request)
122 pull_request = self.__get_pull_request(pull_request)
123 current_reviewers = PullRequestReviewers.query()\
123 current_reviewers = PullRequestReviewers.query()\
124 .filter(PullRequestReviewers.pull_request==
124 .filter(PullRequestReviewers.pull_request==
125 pull_request)\
125 pull_request)\
126 .all()
126 .all()
127 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
127 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
128
128
129 to_add = reviewers_ids.difference(current_reviewers_ids)
129 to_add = reviewers_ids.difference(current_reviewers_ids)
130 to_remove = current_reviewers_ids.difference(reviewers_ids)
130 to_remove = current_reviewers_ids.difference(reviewers_ids)
131
131
132 log.debug("Adding %s reviewers" % to_add)
132 log.debug("Adding %s reviewers" % to_add)
133 log.debug("Removing %s reviewers" % to_remove)
133 log.debug("Removing %s reviewers" % to_remove)
134
134
135 for uid in to_add:
135 for uid in to_add:
136 _usr = self._get_user(uid)
136 _usr = self._get_user(uid)
137 reviewer = PullRequestReviewers(_usr, pull_request)
137 reviewer = PullRequestReviewers(_usr, pull_request)
138 self.sa.add(reviewer)
138 self.sa.add(reviewer)
139
139
140 for uid in to_remove:
140 for uid in to_remove:
141 reviewer = PullRequestReviewers.query()\
141 reviewer = PullRequestReviewers.query()\
142 .filter(PullRequestReviewers.user_id==uid,
142 .filter(PullRequestReviewers.user_id==uid,
143 PullRequestReviewers.pull_request==pull_request)\
143 PullRequestReviewers.pull_request==pull_request)\
144 .scalar()
144 .scalar()
145 if reviewer:
145 if reviewer:
146 self.sa.delete(reviewer)
146 self.sa.delete(reviewer)
147
147
148 def delete(self, pull_request):
148 def delete(self, pull_request):
149 pull_request = self.__get_pull_request(pull_request)
149 pull_request = self.__get_pull_request(pull_request)
150 Session().delete(pull_request)
150 Session().delete(pull_request)
151
151
152 def close_pull_request(self, pull_request):
152 def close_pull_request(self, pull_request):
153 pull_request = self.__get_pull_request(pull_request)
153 pull_request = self.__get_pull_request(pull_request)
154 pull_request.status = PullRequest.STATUS_CLOSED
154 pull_request.status = PullRequest.STATUS_CLOSED
155 pull_request.updated_on = datetime.datetime.now()
155 pull_request.updated_on = datetime.datetime.now()
156 self.sa.add(pull_request)
156 self.sa.add(pull_request)
157
157
158 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref,
158 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref):
159 discovery_data):
160 """
159 """
161 Returns a list of changesets that are incoming from org_repo@org_ref
160 Returns a list of changesets that are incoming from org_repo@org_ref
162 to other_repo@other_ref
161 to other_repo@other_ref
163
162
164 :param org_repo:
163 :param org_repo:
165 :param org_ref:
164 :param org_ref:
166 :param other_repo:
165 :param other_repo:
167 :param other_ref:
166 :param other_ref:
168 :param tmp:
167 :param tmp:
169 """
168 """
170
169
171 changesets = []
170 changesets = []
172 #case two independent repos
171 #case two independent repos
173 common, incoming, rheads = discovery_data
174 if org_repo != other_repo:
172 if org_repo != other_repo:
175 revs = [
173 revs = [
176 org_repo._repo.lookup(org_ref[1]),
174 org_repo._repo.lookup(org_ref[1]),
177 org_repo._repo.lookup(other_ref[1]),
175 org_repo._repo.lookup(other_ref[1]),
178 ]
176 ]
179
177
180 obj = findcommonoutgoing(org_repo._repo,
178 obj = findcommonoutgoing(org_repo._repo,
181 localrepo.locallegacypeer(other_repo._repo.local()),
179 localrepo.locallegacypeer(other_repo._repo.local()),
182 revs,
180 revs,
183 force=True)
181 force=True)
184 revs = obj.missing
182 revs = obj.missing
185
183
186 for cs in map(binascii.hexlify, revs):
184 for cs in map(binascii.hexlify, revs):
187 _cs = org_repo.get_changeset(cs)
185 _cs = org_repo.get_changeset(cs)
188 changesets.append(_cs)
186 changesets.append(_cs)
189 # in case we have revisions filter out the ones not in given range
187 # in case we have revisions filter out the ones not in given range
190 if org_ref[0] == 'rev' and other_ref[0] == 'rev':
188 if org_ref[0] == 'rev' and other_ref[0] == 'rev':
191 revs = [x.raw_id for x in changesets]
189 revs = [x.raw_id for x in changesets]
192 start = org_ref[1]
190 start = org_ref[1]
193 stop = other_ref[1]
191 stop = other_ref[1]
194 changesets = changesets[revs.index(start):revs.index(stop) + 1]
192 changesets = changesets[revs.index(start):revs.index(stop) + 1]
195 else:
193 else:
196 #no remote compare do it on the same repository
194 #no remote compare do it on the same repository
197 if alias == 'hg':
195 if alias == 'hg':
198 _revset_predicates = {
196 _revset_predicates = {
199 'branch': 'branch',
197 'branch': 'branch',
200 'book': 'bookmark',
198 'book': 'bookmark',
201 'tag': 'tag',
199 'tag': 'tag',
202 'rev': 'id',
200 'rev': 'id',
203 }
201 }
204
202
205 revs = [
203 revs = [
206 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
204 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
207 _revset_predicates[other_ref[0]], other_ref[1],
205 _revset_predicates[other_ref[0]], other_ref[1],
208 _revset_predicates[org_ref[0]], org_ref[1],
206 _revset_predicates[org_ref[0]], org_ref[1],
209 )
207 )
210 ]
208 ]
211
209
212 out = scmutil.revrange(org_repo._repo, revs)
210 out = scmutil.revrange(org_repo._repo, revs)
213 for cs in (out):
211 for cs in (out):
214 changesets.append(org_repo.get_changeset(cs))
212 changesets.append(org_repo.get_changeset(cs))
215 elif alias == 'git':
213 elif alias == 'git':
216 so, se = org_repo.run_git_command(
214 so, se = org_repo.run_git_command(
217 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
215 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
218 other_ref[1])
216 other_ref[1])
219 )
217 )
220 ids = re.findall(r'[0-9a-fA-F]{40}', so)
218 ids = re.findall(r'[0-9a-fA-F]{40}', so)
221 for cs in (ids):
219 for cs in (ids):
222 changesets.append(org_repo.get_changeset(cs))
220 changesets.append(org_repo.get_changeset(cs))
223
221
224 return changesets
222 return changesets
225
223
226 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
227 """
228 Get's mercurial discovery data used to calculate difference between
229 repos and refs
230
231 :param org_repo:
232 :type org_repo:
233 :param org_ref:
234 :type org_ref:
235 :param other_repo:
236 :type other_repo:
237 :param other_ref:
238 :type other_ref:
239 """
240
241 _org_repo = org_repo._repo
242 org_rev_type, org_rev = org_ref
243
244 _other_repo = other_repo._repo
245 other_rev_type, other_rev = other_ref
246
247 log.debug('Doing discovery for %s@%s vs %s@%s' % (
248 org_repo, org_ref, other_repo, other_ref)
249 )
250
251 #log.debug('Filter heads are %s[%s]' % ('', org_ref[1]))
252 org_peer = localrepo.locallegacypeer(_org_repo.local())
253 tmp = discovery.findcommonincoming(
254 repo=_other_repo, # other_repo we check for incoming
255 remote=org_peer, # org_repo source for incoming
256 # heads=[_other_repo[other_rev].node(),
257 # _org_repo[org_rev].node()],
258 force=True
259 )
260 return tmp
261
262 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
224 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
263 """
225 """
264 Returns a tuple of incomming changesets, and discoverydata cache for
226 Returns incomming changesets for mercurial repositories
265 mercurial repositories
266
227
267 :param org_repo:
228 :param org_repo:
268 :type org_repo:
229 :type org_repo:
269 :param org_ref:
230 :param org_ref:
270 :type org_ref:
231 :type org_ref:
271 :param other_repo:
232 :param other_repo:
272 :type other_repo:
233 :type other_repo:
273 :param other_ref:
234 :param other_ref:
274 :type other_ref:
235 :type other_ref:
275 """
236 """
276
237
277 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
238 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
278 raise Exception('org_ref must be a two element list/tuple')
239 raise Exception('org_ref must be a two element list/tuple')
279
240
280 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
241 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
281 raise Exception('other_ref must be a two element list/tuple')
242 raise Exception('other_ref must be a two element list/tuple')
282
243
283 org_repo_scm = org_repo.scm_instance
244 org_repo_scm = org_repo.scm_instance
284 other_repo_scm = other_repo.scm_instance
245 other_repo_scm = other_repo.scm_instance
285
246
286 alias = org_repo.scm_instance.alias
247 alias = org_repo.scm_instance.alias
287 discovery_data = [None, None, None]
288 if alias == 'hg':
289 discovery_data = self._get_discovery(org_repo_scm, org_ref,
290 other_repo_scm, other_ref)
291 cs_ranges = self._get_changesets(alias,
248 cs_ranges = self._get_changesets(alias,
292 org_repo_scm, org_ref,
249 org_repo_scm, org_ref,
293 other_repo_scm, other_ref,
250 other_repo_scm, other_ref)
294 discovery_data)
251 return cs_ranges
295 return cs_ranges, discovery_data
General Comments 0
You need to be logged in to leave comments. Login now