##// END OF EJS Templates
Created base for diffing two repositories inside rhodecode
marcink -
r2337:f8c953c6 codereview
parent child Browse files
Show More
@@ -0,0 +1,82 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
3
4 <%def name="title()">
5 TODO FIll this in
6 </%def>
7
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(u'Home',h.url('/'))}
10 &raquo;
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 &raquo;
13 TODO!
14 </%def>
15
16 <%def name="page_nav()">
17 ${self.menu('changelog')}
18 </%def>
19
20 <%def name="main()">
21 <div class="box">
22 <!-- box / title -->
23 <div class="title">
24 ${self.breadcrumbs()}
25 </div>
26 <div class="table">
27 <div id="body" class="diffblock">
28 <div class="code-header cv">
29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 <div>
31 ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
32 </div>
33 </div>
34 </div>
35 <div id="changeset_compare_view_content">
36 <div class="container">
37 <table class="compare_view_commits noborder">
38 %for cnt,cs in enumerate(c.cs_ranges):
39 <tr>
40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
42 <td><div class="author">${h.person(cs.author)}</div></td>
43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
44 <td>
45 %if hasattr(c,'statuses') and c.statuses:
46 <div title="${_('Changeset status')}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cnt])}" /></div>
47 %endif
48 </td>
49 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
50 </tr>
51 %endfor
52 </table>
53 </div>
54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
55 <div class="cs_files">
56 %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]:
57 <div class="cs_${change}">${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}</div>
58 %endfor
59 </div>
60 </div>
61
62 </div>
63
64 <script type="text/javascript">
65
66 YUE.onDOMReady(function(){
67
68 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
69 var act = e.currentTarget.nextElementSibling;
70
71 if(YUD.hasClass(act,'active')){
72 YUD.removeClass(act,'active');
73 YUD.setStyle(act,'display','none');
74 }else{
75 YUD.addClass(act,'active');
76 YUD.setStyle(act,'display','');
77 }
78 });
79 })
80 </script>
81 </div>
82 </%def>
@@ -1,95 +1,130 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 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 pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, session, tmpl_context as c, url
30 from pylons.controllers.util import abort, redirect
31 from pylons.controllers.util import abort, redirect
31
32
32 from rhodecode.lib.base import BaseRepoController, render
33 from rhodecode.lib.base import BaseRepoController, render
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from webob.exc import HTTPNotFound
35 from rhodecode.lib import diffs
36
37 from rhodecode.model.db import Repository
35
38
36 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
37
40
38
41
39 class CompareController(BaseRepoController):
42 class CompareController(BaseRepoController):
40
43
41 @LoginRequired()
44 @LoginRequired()
42 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
45 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
43 'repository.admin')
46 'repository.admin')
44 def __before__(self):
47 def __before__(self):
45 super(CompareController, self).__before__()
48 super(CompareController, self).__before__()
46
49
47 def _handle_ref(self, ref):
50 def _handle_ref(self, ref):
48 """
51 """
49 Parse the org...other string
52 Parse the org...other string
50 Possible formats are `(branch|book|tag):<name>...(branch|book|tag):<othername>`
53 Possible formats are
54 `(branch|book|tag):<name>...(branch|book|tag):<othername>`
51
55
52 :param ref: <orginal_reference>...<other_reference>
56 :param ref: <orginal_reference>...<other_reference>
53 :type ref: str
57 :type ref: str
54 """
58 """
55 org_repo = c.rhodecode_repo.name
59 org_repo = c.rhodecode_repo.name
56
60
57 def org_parser(org):
61 def org_parser(org):
58 _repo = org_repo
62 _repo = org_repo
59 name, val = org.split(':')
63 name, val = org.split(':')
60 return _repo, (name, val)
64 return _repo, (name, val)
61
65
62 def other_parser(other):
66 def other_parser(other):
63 _other_repo = request.GET.get('repo')
67 _other_repo = request.GET.get('repo')
64 _repo = org_repo
68 _repo = org_repo
65 name, val = other.split(':')
69 name, val = other.split(':')
66 if _other_repo:
70 if _other_repo:
67 _repo = _other_repo #TODO: do an actual repo loookup within rhodecode
71 #TODO: do an actual repo loookup within rhodecode
72 _repo = _other_repo
68
73
69 return _repo, (name, val)
74 return _repo, (name, val)
70
75
71 if '...' in ref:
76 if '...' in ref:
72 try:
77 try:
73 org, other = ref.split('...')
78 org, other = ref.split('...')
74 org_repo, org_ref = org_parser(org)
79 org_repo, org_ref = org_parser(org)
75 other_repo, other_ref = other_parser(other)
80 other_repo, other_ref = other_parser(other)
76 return org_repo, org_ref, other_repo, other_ref
81 return org_repo, org_ref, other_repo, other_ref
77 except:
82 except:
78 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
79
84
80 raise HTTPNotFound
85 raise HTTPNotFound
81
86
87 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref):
88 changesets = []
89 #case two independent repos
90 if org_repo != other_repo:
91 from mercurial import discovery
92 import binascii
93 out = discovery.findcommonoutgoing(org_repo._repo, other_repo._repo)
94 for cs in map(binascii.hexlify, out.missing):
95 changesets.append(org_repo.get_changeset(cs))
96 else:
97 for cs in map(binascii.hexlify, out):
98 changesets.append(org_repo.get_changeset(cs))
99
100 return changesets
101
82 def index(self, ref):
102 def index(self, ref):
83
84 org_repo, org_ref, other_repo, other_ref = self._handle_ref(ref)
103 org_repo, org_ref, other_repo, other_ref = self._handle_ref(ref)
85 return '''
104
86 <pre>
105 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
87 REPO: %s
106 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
88 REF: %s
107
108 c.cs_ranges = self._get_changesets(org_repo.scm_instance,
109 org_ref,
110 other_repo.scm_instance,
111 other_ref)
112
113 c.org_ref = org_ref[1]
114 c.other_ref = other_ref[1]
115 cs1 = org_repo.scm_instance.get_changeset(org_ref[1])
116 cs2 = other_repo.scm_instance.get_changeset(other_ref[1])
117
118 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref)
119 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
120
121 diff = diff_processor.as_html(enable_comments=False)
122 stats = diff_processor.stat()
123
124 c.changes = [('change?', None, diff, cs1, cs2, stats,)]
125
126 return render('compare/compare_diff.html')
127
128
129
89
130
90 vs
91
92 REPO: %s
93 REF: %s
94 </pre>
95 ''' % (org_repo, org_ref, other_repo, other_ref)
@@ -1,524 +1,553 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 markupsafe
30 import markupsafe
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34
34
35 from rhodecode.lib.vcs.exceptions import VCSError
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.helpers import escape
37 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.utils import EmptyChangeset
39
39
40
40
41 def wrap_to_table(str_):
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
46 </tr>
47 </table>''' % str_
47 </table>''' % str_
48
48
49
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
52 enable_comments=False):
53 """
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
55 proper message
56 """
56 """
57
57
58 if filenode_old is None:
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
60
61 if filenode_old.is_binary or filenode_new.is_binary:
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
63 stats = (0, 0)
64 size = 0
64 size = 0
65
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
75 stats = diff_processor.stat()
76 size = len(diff or '')
76 size = len(diff or '')
77 else:
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
79 'diff menu to display this diff'))
80 stats = (0, 0)
80 stats = (0, 0)
81 size = 0
81 size = 0
82 if not diff:
82 if not diff:
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
84 [filenode_new, filenode_old])
84 [filenode_new, filenode_old])
85 if submodules:
85 if submodules:
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
87 else:
87 else:
88 diff = wrap_to_table(_('No changes detected'))
88 diff = wrap_to_table(_('No changes detected'))
89
89
90 cs1 = filenode_old.changeset.raw_id
90 cs1 = filenode_old.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
92
92
93 return size, cs1, cs2, diff, stats
93 return size, cs1, cs2, diff, stats
94
94
95
95
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
97 """
97 """
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
99
99
100 :param ignore_whitespace: ignore whitespaces in diff
100 :param ignore_whitespace: ignore whitespaces in diff
101 """
101 """
102 # make sure we pass in default context
102 # make sure we pass in default context
103 context = context or 3
103 context = context or 3
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
105 [filenode_new, filenode_old])
106 if submodules:
106 if submodules:
107 return ''
107 return ''
108
108
109 for filenode in (filenode_old, filenode_new):
109 for filenode in (filenode_old, filenode_new):
110 if not isinstance(filenode, FileNode):
110 if not isinstance(filenode, FileNode):
111 raise VCSError("Given object should be FileNode object, not %s"
111 raise VCSError("Given object should be FileNode object, not %s"
112 % filenode.__class__)
112 % filenode.__class__)
113
113
114 repo = filenode_new.changeset.repository
114 repo = filenode_new.changeset.repository
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
116 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
117
117
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
119 ignore_whitespace, context)
119 ignore_whitespace, context)
120 return vcs_gitdiff
120 return vcs_gitdiff
121
121
122
122
123 class DiffProcessor(object):
123 class DiffProcessor(object):
124 """
124 """
125 Give it a unified diff and it returns a list of the files that were
125 Give it a unified diff and it returns a list of the files that were
126 mentioned in the diff together with a dict of meta information that
126 mentioned in the diff together with a dict of meta information that
127 can be used to render it in a HTML template.
127 can be used to render it in a HTML template.
128 """
128 """
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
130
130
131 def __init__(self, diff, differ='diff', format='udiff'):
131 def __init__(self, diff, differ='diff', format='udiff'):
132 """
132 """
133 :param diff: a text in diff format or generator
133 :param diff: a text in diff format or generator
134 :param format: format of diff passed, `udiff` or `gitdiff`
134 :param format: format of diff passed, `udiff` or `gitdiff`
135 """
135 """
136 if isinstance(diff, basestring):
136 if isinstance(diff, basestring):
137 diff = [diff]
137 diff = [diff]
138
138
139 self.__udiff = diff
139 self.__udiff = diff
140 self.__format = format
140 self.__format = format
141 self.adds = 0
141 self.adds = 0
142 self.removes = 0
142 self.removes = 0
143
143
144 if isinstance(self.__udiff, basestring):
144 if isinstance(self.__udiff, basestring):
145 self.lines = iter(self.__udiff.splitlines(1))
145 self.lines = iter(self.__udiff.splitlines(1))
146
146
147 elif self.__format == 'gitdiff':
147 elif self.__format == 'gitdiff':
148 udiff_copy = self.copy_iterator()
148 udiff_copy = self.copy_iterator()
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
150 else:
150 else:
151 udiff_copy = self.copy_iterator()
151 udiff_copy = self.copy_iterator()
152 self.lines = imap(self.escaper, udiff_copy)
152 self.lines = imap(self.escaper, udiff_copy)
153
153
154 # Select a differ.
154 # Select a differ.
155 if differ == 'difflib':
155 if differ == 'difflib':
156 self.differ = self._highlight_line_difflib
156 self.differ = self._highlight_line_difflib
157 else:
157 else:
158 self.differ = self._highlight_line_udiff
158 self.differ = self._highlight_line_udiff
159
159
160 def escaper(self, string):
160 def escaper(self, string):
161 return markupsafe.escape(string)
161 return markupsafe.escape(string)
162
162
163 def copy_iterator(self):
163 def copy_iterator(self):
164 """
164 """
165 make a fresh copy of generator, we should not iterate thru
165 make a fresh copy of generator, we should not iterate thru
166 an original as it's needed for repeating operations on
166 an original as it's needed for repeating operations on
167 this instance of DiffProcessor
167 this instance of DiffProcessor
168 """
168 """
169 self.__udiff, iterator_copy = tee(self.__udiff)
169 self.__udiff, iterator_copy = tee(self.__udiff)
170 return iterator_copy
170 return iterator_copy
171
171
172 def _extract_rev(self, line1, line2):
172 def _extract_rev(self, line1, line2):
173 """
173 """
174 Extract the filename and revision hint from a line.
174 Extract the filename and revision hint from a line.
175 """
175 """
176
176
177 try:
177 try:
178 if line1.startswith('--- ') and line2.startswith('+++ '):
178 if line1.startswith('--- ') and line2.startswith('+++ '):
179 l1 = line1[4:].split(None, 1)
179 l1 = line1[4:].split(None, 1)
180 old_filename = (l1[0].replace('a/', '', 1)
180 old_filename = (l1[0].replace('a/', '', 1)
181 if len(l1) >= 1 else None)
181 if len(l1) >= 1 else None)
182 old_rev = l1[1] if len(l1) == 2 else 'old'
182 old_rev = l1[1] if len(l1) == 2 else 'old'
183
183
184 l2 = line2[4:].split(None, 1)
184 l2 = line2[4:].split(None, 1)
185 new_filename = (l2[0].replace('b/', '', 1)
185 new_filename = (l2[0].replace('b/', '', 1)
186 if len(l1) >= 1 else None)
186 if len(l1) >= 1 else None)
187 new_rev = l2[1] if len(l2) == 2 else 'new'
187 new_rev = l2[1] if len(l2) == 2 else 'new'
188
188
189 filename = (old_filename
189 filename = (old_filename
190 if old_filename != '/dev/null' else new_filename)
190 if old_filename != '/dev/null' else new_filename)
191
191
192 return filename, new_rev, old_rev
192 return filename, new_rev, old_rev
193 except (ValueError, IndexError):
193 except (ValueError, IndexError):
194 pass
194 pass
195
195
196 return None, None, None
196 return None, None, None
197
197
198 def _parse_gitdiff(self, diffiterator):
198 def _parse_gitdiff(self, diffiterator):
199 def line_decoder(l):
199 def line_decoder(l):
200 if l.startswith('+') and not l.startswith('+++'):
200 if l.startswith('+') and not l.startswith('+++'):
201 self.adds += 1
201 self.adds += 1
202 elif l.startswith('-') and not l.startswith('---'):
202 elif l.startswith('-') and not l.startswith('---'):
203 self.removes += 1
203 self.removes += 1
204 return l.decode('utf8', 'replace')
204 return l.decode('utf8', 'replace')
205
205
206 output = list(diffiterator)
206 output = list(diffiterator)
207 size = len(output)
207 size = len(output)
208
208
209 if size == 2:
209 if size == 2:
210 l = []
210 l = []
211 l.extend([output[0]])
211 l.extend([output[0]])
212 l.extend(output[1].splitlines(1))
212 l.extend(output[1].splitlines(1))
213 return map(line_decoder, l)
213 return map(line_decoder, l)
214 elif size == 1:
214 elif size == 1:
215 return map(line_decoder, output[0].splitlines(1))
215 return map(line_decoder, output[0].splitlines(1))
216 elif size == 0:
216 elif size == 0:
217 return []
217 return []
218
218
219 raise Exception('wrong size of diff %s' % size)
219 raise Exception('wrong size of diff %s' % size)
220
220
221 def _highlight_line_difflib(self, line, next_):
221 def _highlight_line_difflib(self, line, next_):
222 """
222 """
223 Highlight inline changes in both lines.
223 Highlight inline changes in both lines.
224 """
224 """
225
225
226 if line['action'] == 'del':
226 if line['action'] == 'del':
227 old, new = line, next_
227 old, new = line, next_
228 else:
228 else:
229 old, new = next_, line
229 old, new = next_, line
230
230
231 oldwords = re.split(r'(\W)', old['line'])
231 oldwords = re.split(r'(\W)', old['line'])
232 newwords = re.split(r'(\W)', new['line'])
232 newwords = re.split(r'(\W)', new['line'])
233
233
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
235
235
236 oldfragments, newfragments = [], []
236 oldfragments, newfragments = [], []
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
238 oldfrag = ''.join(oldwords[i1:i2])
238 oldfrag = ''.join(oldwords[i1:i2])
239 newfrag = ''.join(newwords[j1:j2])
239 newfrag = ''.join(newwords[j1:j2])
240 if tag != 'equal':
240 if tag != 'equal':
241 if oldfrag:
241 if oldfrag:
242 oldfrag = '<del>%s</del>' % oldfrag
242 oldfrag = '<del>%s</del>' % oldfrag
243 if newfrag:
243 if newfrag:
244 newfrag = '<ins>%s</ins>' % newfrag
244 newfrag = '<ins>%s</ins>' % newfrag
245 oldfragments.append(oldfrag)
245 oldfragments.append(oldfrag)
246 newfragments.append(newfrag)
246 newfragments.append(newfrag)
247
247
248 old['line'] = "".join(oldfragments)
248 old['line'] = "".join(oldfragments)
249 new['line'] = "".join(newfragments)
249 new['line'] = "".join(newfragments)
250
250
251 def _highlight_line_udiff(self, line, next_):
251 def _highlight_line_udiff(self, line, next_):
252 """
252 """
253 Highlight inline changes in both lines.
253 Highlight inline changes in both lines.
254 """
254 """
255 start = 0
255 start = 0
256 limit = min(len(line['line']), len(next_['line']))
256 limit = min(len(line['line']), len(next_['line']))
257 while start < limit and line['line'][start] == next_['line'][start]:
257 while start < limit and line['line'][start] == next_['line'][start]:
258 start += 1
258 start += 1
259 end = -1
259 end = -1
260 limit -= start
260 limit -= start
261 while -end <= limit and line['line'][end] == next_['line'][end]:
261 while -end <= limit and line['line'][end] == next_['line'][end]:
262 end -= 1
262 end -= 1
263 end += 1
263 end += 1
264 if start or end:
264 if start or end:
265 def do(l):
265 def do(l):
266 last = end + len(l['line'])
266 last = end + len(l['line'])
267 if l['action'] == 'add':
267 if l['action'] == 'add':
268 tag = 'ins'
268 tag = 'ins'
269 else:
269 else:
270 tag = 'del'
270 tag = 'del'
271 l['line'] = '%s<%s>%s</%s>%s' % (
271 l['line'] = '%s<%s>%s</%s>%s' % (
272 l['line'][:start],
272 l['line'][:start],
273 tag,
273 tag,
274 l['line'][start:last],
274 l['line'][start:last],
275 tag,
275 tag,
276 l['line'][last:]
276 l['line'][last:]
277 )
277 )
278 do(line)
278 do(line)
279 do(next_)
279 do(next_)
280
280
281 def _parse_udiff(self):
281 def _parse_udiff(self):
282 """
282 """
283 Parse the diff an return data for the template.
283 Parse the diff an return data for the template.
284 """
284 """
285 lineiter = self.lines
285 lineiter = self.lines
286 files = []
286 files = []
287 try:
287 try:
288 line = lineiter.next()
288 line = lineiter.next()
289 # skip first context
289 # skip first context
290 skipfirst = True
290 skipfirst = True
291 while 1:
291 while 1:
292 # continue until we found the old file
292 # continue until we found the old file
293 if not line.startswith('--- '):
293 if not line.startswith('--- '):
294 line = lineiter.next()
294 line = lineiter.next()
295 continue
295 continue
296
296
297 chunks = []
297 chunks = []
298 filename, old_rev, new_rev = \
298 filename, old_rev, new_rev = \
299 self._extract_rev(line, lineiter.next())
299 self._extract_rev(line, lineiter.next())
300 files.append({
300 files.append({
301 'filename': filename,
301 'filename': filename,
302 'old_revision': old_rev,
302 'old_revision': old_rev,
303 'new_revision': new_rev,
303 'new_revision': new_rev,
304 'chunks': chunks
304 'chunks': chunks
305 })
305 })
306
306
307 line = lineiter.next()
307 line = lineiter.next()
308 while line:
308 while line:
309 match = self._chunk_re.match(line)
309 match = self._chunk_re.match(line)
310 if not match:
310 if not match:
311 break
311 break
312
312
313 lines = []
313 lines = []
314 chunks.append(lines)
314 chunks.append(lines)
315
315
316 old_line, old_end, new_line, new_end = \
316 old_line, old_end, new_line, new_end = \
317 [int(x or 1) for x in match.groups()[:-1]]
317 [int(x or 1) for x in match.groups()[:-1]]
318 old_line -= 1
318 old_line -= 1
319 new_line -= 1
319 new_line -= 1
320 context = len(match.groups()) == 5
320 context = len(match.groups()) == 5
321 old_end += old_line
321 old_end += old_line
322 new_end += new_line
322 new_end += new_line
323
323
324 if context:
324 if context:
325 if not skipfirst:
325 if not skipfirst:
326 lines.append({
326 lines.append({
327 'old_lineno': '...',
327 'old_lineno': '...',
328 'new_lineno': '...',
328 'new_lineno': '...',
329 'action': 'context',
329 'action': 'context',
330 'line': line,
330 'line': line,
331 })
331 })
332 else:
332 else:
333 skipfirst = False
333 skipfirst = False
334
334
335 line = lineiter.next()
335 line = lineiter.next()
336 while old_line < old_end or new_line < new_end:
336 while old_line < old_end or new_line < new_end:
337 if line:
337 if line:
338 command, line = line[0], line[1:]
338 command, line = line[0], line[1:]
339 else:
339 else:
340 command = ' '
340 command = ' '
341 affects_old = affects_new = False
341 affects_old = affects_new = False
342
342
343 # ignore those if we don't expect them
343 # ignore those if we don't expect them
344 if command in '#@':
344 if command in '#@':
345 continue
345 continue
346 elif command == '+':
346 elif command == '+':
347 affects_new = True
347 affects_new = True
348 action = 'add'
348 action = 'add'
349 elif command == '-':
349 elif command == '-':
350 affects_old = True
350 affects_old = True
351 action = 'del'
351 action = 'del'
352 else:
352 else:
353 affects_old = affects_new = True
353 affects_old = affects_new = True
354 action = 'unmod'
354 action = 'unmod'
355
355
356 old_line += affects_old
356 old_line += affects_old
357 new_line += affects_new
357 new_line += affects_new
358 lines.append({
358 lines.append({
359 'old_lineno': affects_old and old_line or '',
359 'old_lineno': affects_old and old_line or '',
360 'new_lineno': affects_new and new_line or '',
360 'new_lineno': affects_new and new_line or '',
361 'action': action,
361 'action': action,
362 'line': line
362 'line': line
363 })
363 })
364 line = lineiter.next()
364 line = lineiter.next()
365
365
366 except StopIteration:
366 except StopIteration:
367 pass
367 pass
368
368
369 # highlight inline changes
369 # highlight inline changes
370 for _ in files:
370 for _ in files:
371 for chunk in chunks:
371 for chunk in chunks:
372 lineiter = iter(chunk)
372 lineiter = iter(chunk)
373 #first = True
373 #first = True
374 try:
374 try:
375 while 1:
375 while 1:
376 line = lineiter.next()
376 line = lineiter.next()
377 if line['action'] != 'unmod':
377 if line['action'] != 'unmod':
378 nextline = lineiter.next()
378 nextline = lineiter.next()
379 if nextline['action'] == 'unmod' or \
379 if nextline['action'] == 'unmod' or \
380 nextline['action'] == line['action']:
380 nextline['action'] == line['action']:
381 continue
381 continue
382 self.differ(line, nextline)
382 self.differ(line, nextline)
383 except StopIteration:
383 except StopIteration:
384 pass
384 pass
385
385
386 return files
386 return files
387
387
388 def prepare(self):
388 def prepare(self):
389 """
389 """
390 Prepare the passed udiff for HTML rendering. It'l return a list
390 Prepare the passed udiff for HTML rendering. It'l return a list
391 of dicts
391 of dicts
392 """
392 """
393 return self._parse_udiff()
393 return self._parse_udiff()
394
394
395 def _safe_id(self, idstring):
395 def _safe_id(self, idstring):
396 """Make a string safe for including in an id attribute.
396 """Make a string safe for including in an id attribute.
397
397
398 The HTML spec says that id attributes 'must begin with
398 The HTML spec says that id attributes 'must begin with
399 a letter ([A-Za-z]) and may be followed by any number
399 a letter ([A-Za-z]) and may be followed by any number
400 of letters, digits ([0-9]), hyphens ("-"), underscores
400 of letters, digits ([0-9]), hyphens ("-"), underscores
401 ("_"), colons (":"), and periods (".")'. These regexps
401 ("_"), colons (":"), and periods (".")'. These regexps
402 are slightly over-zealous, in that they remove colons
402 are slightly over-zealous, in that they remove colons
403 and periods unnecessarily.
403 and periods unnecessarily.
404
404
405 Whitespace is transformed into underscores, and then
405 Whitespace is transformed into underscores, and then
406 anything which is not a hyphen or a character that
406 anything which is not a hyphen or a character that
407 matches \w (alphanumerics and underscore) is removed.
407 matches \w (alphanumerics and underscore) is removed.
408
408
409 """
409 """
410 # Transform all whitespace to underscore
410 # Transform all whitespace to underscore
411 idstring = re.sub(r'\s', "_", '%s' % idstring)
411 idstring = re.sub(r'\s', "_", '%s' % idstring)
412 # Remove everything that is not a hyphen or a member of \w
412 # Remove everything that is not a hyphen or a member of \w
413 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
413 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
414 return idstring
414 return idstring
415
415
416 def raw_diff(self):
416 def raw_diff(self):
417 """
417 """
418 Returns raw string as udiff
418 Returns raw string as udiff
419 """
419 """
420 udiff_copy = self.copy_iterator()
420 udiff_copy = self.copy_iterator()
421 if self.__format == 'gitdiff':
421 if self.__format == 'gitdiff':
422 udiff_copy = self._parse_gitdiff(udiff_copy)
422 udiff_copy = self._parse_gitdiff(udiff_copy)
423 return u''.join(udiff_copy)
423 return u''.join(udiff_copy)
424
424
425 def as_html(self, table_class='code-difftable', line_class='line',
425 def as_html(self, table_class='code-difftable', line_class='line',
426 new_lineno_class='lineno old', old_lineno_class='lineno new',
426 new_lineno_class='lineno old', old_lineno_class='lineno new',
427 code_class='code', enable_comments=False):
427 code_class='code', enable_comments=False):
428 """
428 """
429 Return udiff as html table with customized css classes
429 Return udiff as html table with customized css classes
430 """
430 """
431 def _link_to_if(condition, label, url):
431 def _link_to_if(condition, label, url):
432 """
432 """
433 Generates a link if condition is meet or just the label if not.
433 Generates a link if condition is meet or just the label if not.
434 """
434 """
435
435
436 if condition:
436 if condition:
437 return '''<a href="%(url)s">%(label)s</a>''' % {
437 return '''<a href="%(url)s">%(label)s</a>''' % {
438 'url': url,
438 'url': url,
439 'label': label
439 'label': label
440 }
440 }
441 else:
441 else:
442 return label
442 return label
443 diff_lines = self.prepare()
443 diff_lines = self.prepare()
444 _html_empty = True
444 _html_empty = True
445 _html = []
445 _html = []
446 _html.append('''<table class="%(table_class)s">\n''' % {
446 _html.append('''<table class="%(table_class)s">\n''' % {
447 'table_class': table_class
447 'table_class': table_class
448 })
448 })
449 for diff in diff_lines:
449 for diff in diff_lines:
450 for line in diff['chunks']:
450 for line in diff['chunks']:
451 _html_empty = False
451 _html_empty = False
452 for change in line:
452 for change in line:
453 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
453 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
454 'lc': line_class,
454 'lc': line_class,
455 'action': change['action']
455 'action': change['action']
456 })
456 })
457 anchor_old_id = ''
457 anchor_old_id = ''
458 anchor_new_id = ''
458 anchor_new_id = ''
459 anchor_old = "%(filename)s_o%(oldline_no)s" % {
459 anchor_old = "%(filename)s_o%(oldline_no)s" % {
460 'filename': self._safe_id(diff['filename']),
460 'filename': self._safe_id(diff['filename']),
461 'oldline_no': change['old_lineno']
461 'oldline_no': change['old_lineno']
462 }
462 }
463 anchor_new = "%(filename)s_n%(oldline_no)s" % {
463 anchor_new = "%(filename)s_n%(oldline_no)s" % {
464 'filename': self._safe_id(diff['filename']),
464 'filename': self._safe_id(diff['filename']),
465 'oldline_no': change['new_lineno']
465 'oldline_no': change['new_lineno']
466 }
466 }
467 cond_old = (change['old_lineno'] != '...' and
467 cond_old = (change['old_lineno'] != '...' and
468 change['old_lineno'])
468 change['old_lineno'])
469 cond_new = (change['new_lineno'] != '...' and
469 cond_new = (change['new_lineno'] != '...' and
470 change['new_lineno'])
470 change['new_lineno'])
471 if cond_old:
471 if cond_old:
472 anchor_old_id = 'id="%s"' % anchor_old
472 anchor_old_id = 'id="%s"' % anchor_old
473 if cond_new:
473 if cond_new:
474 anchor_new_id = 'id="%s"' % anchor_new
474 anchor_new_id = 'id="%s"' % anchor_new
475 ###########################################################
475 ###########################################################
476 # OLD LINE NUMBER
476 # OLD LINE NUMBER
477 ###########################################################
477 ###########################################################
478 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
478 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
479 'a_id': anchor_old_id,
479 'a_id': anchor_old_id,
480 'olc': old_lineno_class
480 'olc': old_lineno_class
481 })
481 })
482
482
483 _html.append('''%(link)s''' % {
483 _html.append('''%(link)s''' % {
484 'link': _link_to_if(True, change['old_lineno'],
484 'link': _link_to_if(True, change['old_lineno'],
485 '#%s' % anchor_old)
485 '#%s' % anchor_old)
486 })
486 })
487 _html.append('''</td>\n''')
487 _html.append('''</td>\n''')
488 ###########################################################
488 ###########################################################
489 # NEW LINE NUMBER
489 # NEW LINE NUMBER
490 ###########################################################
490 ###########################################################
491
491
492 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
492 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
493 'a_id': anchor_new_id,
493 'a_id': anchor_new_id,
494 'nlc': new_lineno_class
494 'nlc': new_lineno_class
495 })
495 })
496
496
497 _html.append('''%(link)s''' % {
497 _html.append('''%(link)s''' % {
498 'link': _link_to_if(True, change['new_lineno'],
498 'link': _link_to_if(True, change['new_lineno'],
499 '#%s' % anchor_new)
499 '#%s' % anchor_new)
500 })
500 })
501 _html.append('''</td>\n''')
501 _html.append('''</td>\n''')
502 ###########################################################
502 ###########################################################
503 # CODE
503 # CODE
504 ###########################################################
504 ###########################################################
505 comments = '' if enable_comments else 'no-comment'
505 comments = '' if enable_comments else 'no-comment'
506 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
506 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
507 'cc': code_class,
507 'cc': code_class,
508 'inc': comments
508 'inc': comments
509 })
509 })
510 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
510 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
511 'code': change['line']
511 'code': change['line']
512 })
512 })
513 _html.append('''\t</td>''')
513 _html.append('''\t</td>''')
514 _html.append('''\n</tr>\n''')
514 _html.append('''\n</tr>\n''')
515 _html.append('''</table>''')
515 _html.append('''</table>''')
516 if _html_empty:
516 if _html_empty:
517 return None
517 return None
518 return ''.join(_html)
518 return ''.join(_html)
519
519
520 def stat(self):
520 def stat(self):
521 """
521 """
522 Returns tuple of added, and removed lines for this instance
522 Returns tuple of added, and removed lines for this instance
523 """
523 """
524 return self.adds, self.removes
524 return self.adds, self.removes
525
526
527 def differ(org_repo, org_ref, other_repo, other_ref):
528 """
529
530 :param org_repo:
531 :type org_repo:
532 :param org_ref:
533 :type org_ref:
534 :param other_repo:
535 :type other_repo:
536 :param other_ref:
537 :type other_ref:
538 """
539 ignore_whitespace = False
540 context = 3
541 from mercurial import patch
542 from mercurial.mdiff import diffopts
543
544 org_repo = org_repo.scm_instance._repo
545 other_repo = other_repo.scm_instance._repo
546
547 org_ref = org_ref[1]
548 other_ref = other_ref[1]
549
550 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
551
552 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
553 opts=opts))
General Comments 0
You need to be logged in to leave comments. Login now