##// END OF EJS Templates
added basic comparision of two repositories using bundles...
marcink -
r2355:29a80968 codereview
parent child Browse files
Show More
@@ -1,148 +1,159 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 import binascii
28 import binascii
29
29
30 from webob.exc import HTTPNotFound
30 from webob.exc import HTTPNotFound
31 from pylons import request, response, session, tmpl_context as c, url
31 from pylons import request, response, session, tmpl_context as c, url
32 from pylons.controllers.util import abort, redirect
32 from pylons.controllers.util import abort, redirect
33
33
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.base import BaseRepoController, render
35 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib import diffs
37 from rhodecode.lib import diffs
38
38
39 from rhodecode.model.db import Repository
39 from rhodecode.model.db import Repository
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class CompareController(BaseRepoController):
44 class CompareController(BaseRepoController):
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 'repository.admin')
48 'repository.admin')
49 def __before__(self):
49 def __before__(self):
50 super(CompareController, self).__before__()
50 super(CompareController, self).__before__()
51
51
52 def _handle_ref(self, ref):
52 def _handle_ref(self, ref):
53 """
53 """
54 Parse the org...other string
54 Parse the org...other string
55 Possible formats are
55 Possible formats are
56 `(branch|book|tag):<name>...(branch|book|tag):<othername>`
56 `(branch|book|tag):<name>...(branch|book|tag):<othername>`
57
57
58 :param ref: <orginal_reference>...<other_reference>
58 :param ref: <orginal_reference>...<other_reference>
59 :type ref: str
59 :type ref: str
60 """
60 """
61 org_repo = c.rhodecode_repo.name
61 org_repo = c.rhodecode_repo.name
62
62
63 def org_parser(org):
63 def org_parser(org):
64 _repo = org_repo
64 _repo = org_repo
65 name, val = org.split(':')
65 name, val = org.split(':')
66 return _repo, (name, val)
66 return _repo, (name, val)
67
67
68 def other_parser(other):
68 def other_parser(other):
69 _other_repo = request.GET.get('repo')
69 _other_repo = request.GET.get('repo')
70 _repo = org_repo
70 _repo = org_repo
71 name, val = other.split(':')
71 name, val = other.split(':')
72 if _other_repo:
72 if _other_repo:
73 #TODO: do an actual repo loookup within rhodecode
73 #TODO: do an actual repo loookup within rhodecode
74 _repo = _other_repo
74 _repo = _other_repo
75
75
76 return _repo, (name, val)
76 return _repo, (name, val)
77
77
78 if '...' in ref:
78 if '...' in ref:
79 try:
79 try:
80 org, other = ref.split('...')
80 org, other = ref.split('...')
81 org_repo, org_ref = org_parser(org)
81 org_repo, org_ref = org_parser(org)
82 other_repo, other_ref = other_parser(other)
82 other_repo, other_ref = other_parser(other)
83 return org_repo, org_ref, other_repo, other_ref
83 return org_repo, org_ref, other_repo, other_ref
84 except:
84 except:
85 log.error(traceback.format_exc())
85 log.error(traceback.format_exc())
86
86
87 raise HTTPNotFound
87 raise HTTPNotFound
88
88
89 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref):
89 def _get_discovery(self,org_repo, org_ref, other_repo, other_ref):
90 changesets = []
91 #case two independent repos
92 if org_repo != other_repo:
93 from mercurial import discovery
90 from mercurial import discovery
94 other = org_repo._repo
91 other = org_repo._repo
95 repo = other_repo._repo
92 repo = other_repo._repo
96 onlyheads = None
93 tmp = discovery.findcommonincoming(
97 tmp = discovery.findcommonincoming(repo=repo,
94 repo=repo, # other_repo we check for incoming
98 remote=other,
95 remote=other, # org_repo source for incoming
99 heads=onlyheads, force=False)
96 heads=[other[org_ref[1]].node()],
97 force=False
98 )
99 return tmp
100
101 def _get_changesets(self, org_repo, org_ref, other_repo, other_ref, tmp):
102 changesets = []
103 #case two independent repos
104 if org_repo != other_repo:
100 common, incoming, rheads = tmp
105 common, incoming, rheads = tmp
106
101 if not incoming:
107 if not incoming:
102 revs = []
108 revs = []
103 else:
109 else:
104 revs = other.changelog.findmissing(common, rheads)
110 revs = org_repo._repo.changelog.findmissing(common, rheads)
105
111
106 for cs in map(binascii.hexlify, revs):
112 for cs in reversed(map(binascii.hexlify, revs)):
107 changesets.append(org_repo.get_changeset(cs))
113 changesets.append(org_repo.get_changeset(cs))
108 else:
114 else:
109 revs = ['ancestors(%s) and not ancestors(%s)' % (org_ref[1],
115 revs = ['ancestors(%s) and not ancestors(%s)' % (org_ref[1],
110 other_ref[1])]
116 other_ref[1])]
111 from mercurial import scmutil
117 from mercurial import scmutil
112 out = scmutil.revrange(org_repo._repo, revs)
118 out = scmutil.revrange(org_repo._repo, revs)
113 for cs in reversed(out):
119 for cs in reversed(out):
114 changesets.append(org_repo.get_changeset(cs))
120 changesets.append(org_repo.get_changeset(cs))
115
121
116 return changesets
122 return changesets
117
123
118 def index(self, ref):
124 def index(self, ref):
119 org_repo, org_ref, other_repo, other_ref = self._handle_ref(ref)
125 org_repo, org_ref, other_repo, other_ref = self._handle_ref(ref)
120 c.swap_url = h.url('compare_home', repo_name=c.repo_name,
126 c.swap_url = h.url('compare_home', repo_name=other_repo,
121 ref='%s...%s' % (':'.join(other_ref),
127 ref='%s...%s' % (':'.join(other_ref),
122 ':'.join(org_ref)))
128 ':'.join(org_ref)),
129 repo=org_repo)
123 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
130 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
124 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
131 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
125
132 tmp = self._get_discovery(org_repo.scm_instance,
133 org_ref,
134 other_repo.scm_instance,
135 other_ref)
126 c.cs_ranges = self._get_changesets(org_repo.scm_instance,
136 c.cs_ranges = self._get_changesets(org_repo.scm_instance,
127 org_ref,
137 org_ref,
128 other_repo.scm_instance,
138 other_repo.scm_instance,
129 other_ref)
139 other_ref,
140 tmp)
130
141
131 c.org_ref = org_ref[1]
142 c.org_ref = org_ref[1]
132 c.other_ref = other_ref[1]
143 c.other_ref = other_ref[1]
133 # diff needs to have swapped org with other to generate proper diff
144 # diff needs to have swapped org with other to generate proper diff
134 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref)
145 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref, tmp)
135 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
146 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
136 _parsed = diff_processor.prepare()
147 _parsed = diff_processor.prepare()
137
148
138 c.files = []
149 c.files = []
139 c.changes = {}
150 c.changes = {}
140 # sort Added first then Modified last Deleted files
151 # sort Added first then Modified last Deleted files
141 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
152 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
142 for f in sorted(_parsed, key=sorter):
153 for f in sorted(_parsed, key=sorter):
143 fid = h.FID('', f['filename'])
154 fid = h.FID('', f['filename'])
144 c.files.append([fid, f['operation'], f['filename'], f['stats']])
155 c.files.append([fid, f['operation'], f['filename'], f['stats']])
145 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
156 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
146 c.changes[fid] = [f['operation'], f['filename'], diff]
157 c.changes[fid] = [f['operation'], f['filename'], diff]
147
158
148 return render('compare/compare_diff.html')
159 return render('compare/compare_diff.html')
@@ -1,565 +1,609 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 io
29 import difflib
30 import difflib
30 import markupsafe
31 import markupsafe
32
31 from itertools import tee, imap
33 from itertools import tee, imap
32
34
35 from mercurial import patch
36 from mercurial.mdiff import diffopts
37 from mercurial.bundlerepo import bundlerepository
38 from mercurial import localrepo
39
33 from pylons.i18n.translation import _
40 from pylons.i18n.translation import _
34
41
35 from rhodecode.lib.vcs.exceptions import VCSError
42 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.helpers import escape
44 from rhodecode.lib.helpers import escape
38 from rhodecode.lib.utils import EmptyChangeset
45 from rhodecode.lib.utils import EmptyChangeset, make_ui
39
46
40
47
41 def wrap_to_table(str_):
48 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
49 return '''<table class="code-difftable">
43 <tr class="line no-comment">
50 <tr class="line no-comment">
44 <td class="lineno new"></td>
51 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
52 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
53 </tr>
47 </table>''' % str_
54 </table>''' % str_
48
55
49
56
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
57 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
58 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
59 enable_comments=False):
53 """
60 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
61 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
62 proper message
56 """
63 """
57
64
58 if filenode_old is None:
65 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
66 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
67
61 if filenode_old.is_binary or filenode_new.is_binary:
68 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
69 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
70 stats = (0, 0)
64 size = 0
71 size = 0
65
72
66 elif cut_off_limit != -1 and (cut_off_limit is None or
73 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)):
74 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
75
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
76 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
77 ignore_whitespace=ignore_whitespace,
71 context=line_context)
78 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
79 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
80
74 diff = diff_processor.as_html(enable_comments=enable_comments)
81 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
82 stats = diff_processor.stat()
76 size = len(diff or '')
83 size = len(diff or '')
77 else:
84 else:
78 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
85 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
79 'diff menu to display this diff'))
86 'diff menu to display this diff'))
80 stats = (0, 0)
87 stats = (0, 0)
81 size = 0
88 size = 0
82 if not diff:
89 if not diff:
83 submodules = filter(lambda o: isinstance(o, SubModuleNode),
90 submodules = filter(lambda o: isinstance(o, SubModuleNode),
84 [filenode_new, filenode_old])
91 [filenode_new, filenode_old])
85 if submodules:
92 if submodules:
86 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
93 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
87 else:
94 else:
88 diff = wrap_to_table(_('No changes detected'))
95 diff = wrap_to_table(_('No changes detected'))
89
96
90 cs1 = filenode_old.changeset.raw_id
97 cs1 = filenode_old.changeset.raw_id
91 cs2 = filenode_new.changeset.raw_id
98 cs2 = filenode_new.changeset.raw_id
92
99
93 return size, cs1, cs2, diff, stats
100 return size, cs1, cs2, diff, stats
94
101
95
102
96 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
103 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
97 """
104 """
98 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
105 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
99
106
100 :param ignore_whitespace: ignore whitespaces in diff
107 :param ignore_whitespace: ignore whitespaces in diff
101 """
108 """
102 # make sure we pass in default context
109 # make sure we pass in default context
103 context = context or 3
110 context = context or 3
104 submodules = filter(lambda o: isinstance(o, SubModuleNode),
111 submodules = filter(lambda o: isinstance(o, SubModuleNode),
105 [filenode_new, filenode_old])
112 [filenode_new, filenode_old])
106 if submodules:
113 if submodules:
107 return ''
114 return ''
108
115
109 for filenode in (filenode_old, filenode_new):
116 for filenode in (filenode_old, filenode_new):
110 if not isinstance(filenode, FileNode):
117 if not isinstance(filenode, FileNode):
111 raise VCSError("Given object should be FileNode object, not %s"
118 raise VCSError("Given object should be FileNode object, not %s"
112 % filenode.__class__)
119 % filenode.__class__)
113
120
114 repo = filenode_new.changeset.repository
121 repo = filenode_new.changeset.repository
115 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
122 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)
123 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
117
124
118 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
125 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
119 ignore_whitespace, context)
126 ignore_whitespace, context)
120 return vcs_gitdiff
127 return vcs_gitdiff
121
128
122
129
123 class DiffProcessor(object):
130 class DiffProcessor(object):
124 """
131 """
125 Give it a unified diff and it returns a list of the files that were
132 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
133 mentioned in the diff together with a dict of meta information that
127 can be used to render it in a HTML template.
134 can be used to render it in a HTML template.
128 """
135 """
129 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
136 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
130
137
131 def __init__(self, diff, differ='diff', format='udiff'):
138 def __init__(self, diff, differ='diff', format='udiff'):
132 """
139 """
133 :param diff: a text in diff format or generator
140 :param diff: a text in diff format or generator
134 :param format: format of diff passed, `udiff` or `gitdiff`
141 :param format: format of diff passed, `udiff` or `gitdiff`
135 """
142 """
136 if isinstance(diff, basestring):
143 if isinstance(diff, basestring):
137 diff = [diff]
144 diff = [diff]
138
145
139 self.__udiff = diff
146 self.__udiff = diff
140 self.__format = format
147 self.__format = format
141 self.adds = 0
148 self.adds = 0
142 self.removes = 0
149 self.removes = 0
143
150
144 if isinstance(self.__udiff, basestring):
151 if isinstance(self.__udiff, basestring):
145 self.lines = iter(self.__udiff.splitlines(1))
152 self.lines = iter(self.__udiff.splitlines(1))
146
153
147 elif self.__format == 'gitdiff':
154 elif self.__format == 'gitdiff':
148 udiff_copy = self.copy_iterator()
155 udiff_copy = self.copy_iterator()
149 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
156 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
150 else:
157 else:
151 udiff_copy = self.copy_iterator()
158 udiff_copy = self.copy_iterator()
152 self.lines = imap(self.escaper, udiff_copy)
159 self.lines = imap(self.escaper, udiff_copy)
153
160
154 # Select a differ.
161 # Select a differ.
155 if differ == 'difflib':
162 if differ == 'difflib':
156 self.differ = self._highlight_line_difflib
163 self.differ = self._highlight_line_difflib
157 else:
164 else:
158 self.differ = self._highlight_line_udiff
165 self.differ = self._highlight_line_udiff
159
166
160 def escaper(self, string):
167 def escaper(self, string):
161 return markupsafe.escape(string)
168 return markupsafe.escape(string)
162
169
163 def copy_iterator(self):
170 def copy_iterator(self):
164 """
171 """
165 make a fresh copy of generator, we should not iterate thru
172 make a fresh copy of generator, we should not iterate thru
166 an original as it's needed for repeating operations on
173 an original as it's needed for repeating operations on
167 this instance of DiffProcessor
174 this instance of DiffProcessor
168 """
175 """
169 self.__udiff, iterator_copy = tee(self.__udiff)
176 self.__udiff, iterator_copy = tee(self.__udiff)
170 return iterator_copy
177 return iterator_copy
171
178
172 def _extract_rev(self, line1, line2):
179 def _extract_rev(self, line1, line2):
173 """
180 """
174 Extract the operation (A/M/D), filename and revision hint from a line.
181 Extract the operation (A/M/D), filename and revision hint from a line.
175 """
182 """
176
183
177 try:
184 try:
178 if line1.startswith('--- ') and line2.startswith('+++ '):
185 if line1.startswith('--- ') and line2.startswith('+++ '):
179 l1 = line1[4:].split(None, 1)
186 l1 = line1[4:].split(None, 1)
180 old_filename = (l1[0].replace('a/', '', 1)
187 old_filename = (l1[0].replace('a/', '', 1)
181 if len(l1) >= 1 else None)
188 if len(l1) >= 1 else None)
182 old_rev = l1[1] if len(l1) == 2 else 'old'
189 old_rev = l1[1] if len(l1) == 2 else 'old'
183
190
184 l2 = line2[4:].split(None, 1)
191 l2 = line2[4:].split(None, 1)
185 new_filename = (l2[0].replace('b/', '', 1)
192 new_filename = (l2[0].replace('b/', '', 1)
186 if len(l1) >= 1 else None)
193 if len(l1) >= 1 else None)
187 new_rev = l2[1] if len(l2) == 2 else 'new'
194 new_rev = l2[1] if len(l2) == 2 else 'new'
188
195
189 filename = (old_filename
196 filename = (old_filename
190 if old_filename != '/dev/null' else new_filename)
197 if old_filename != '/dev/null' else new_filename)
191
198
192 operation = 'D' if new_filename == '/dev/null' else None
199 operation = 'D' if new_filename == '/dev/null' else None
193 if not operation:
200 if not operation:
194 operation = 'M' if old_filename != '/dev/null' else 'A'
201 operation = 'M' if old_filename != '/dev/null' else 'A'
195
202
196 return operation, filename, new_rev, old_rev
203 return operation, filename, new_rev, old_rev
197 except (ValueError, IndexError):
204 except (ValueError, IndexError):
198 pass
205 pass
199
206
200 return None, None, None, None
207 return None, None, None, None
201
208
202 def _parse_gitdiff(self, diffiterator):
209 def _parse_gitdiff(self, diffiterator):
203 def line_decoder(l):
210 def line_decoder(l):
204 if l.startswith('+') and not l.startswith('+++'):
211 if l.startswith('+') and not l.startswith('+++'):
205 self.adds += 1
212 self.adds += 1
206 elif l.startswith('-') and not l.startswith('---'):
213 elif l.startswith('-') and not l.startswith('---'):
207 self.removes += 1
214 self.removes += 1
208 return l.decode('utf8', 'replace')
215 return l.decode('utf8', 'replace')
209
216
210 output = list(diffiterator)
217 output = list(diffiterator)
211 size = len(output)
218 size = len(output)
212
219
213 if size == 2:
220 if size == 2:
214 l = []
221 l = []
215 l.extend([output[0]])
222 l.extend([output[0]])
216 l.extend(output[1].splitlines(1))
223 l.extend(output[1].splitlines(1))
217 return map(line_decoder, l)
224 return map(line_decoder, l)
218 elif size == 1:
225 elif size == 1:
219 return map(line_decoder, output[0].splitlines(1))
226 return map(line_decoder, output[0].splitlines(1))
220 elif size == 0:
227 elif size == 0:
221 return []
228 return []
222
229
223 raise Exception('wrong size of diff %s' % size)
230 raise Exception('wrong size of diff %s' % size)
224
231
225 def _highlight_line_difflib(self, line, next_):
232 def _highlight_line_difflib(self, line, next_):
226 """
233 """
227 Highlight inline changes in both lines.
234 Highlight inline changes in both lines.
228 """
235 """
229
236
230 if line['action'] == 'del':
237 if line['action'] == 'del':
231 old, new = line, next_
238 old, new = line, next_
232 else:
239 else:
233 old, new = next_, line
240 old, new = next_, line
234
241
235 oldwords = re.split(r'(\W)', old['line'])
242 oldwords = re.split(r'(\W)', old['line'])
236 newwords = re.split(r'(\W)', new['line'])
243 newwords = re.split(r'(\W)', new['line'])
237
244
238 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
245 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
239
246
240 oldfragments, newfragments = [], []
247 oldfragments, newfragments = [], []
241 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
248 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
242 oldfrag = ''.join(oldwords[i1:i2])
249 oldfrag = ''.join(oldwords[i1:i2])
243 newfrag = ''.join(newwords[j1:j2])
250 newfrag = ''.join(newwords[j1:j2])
244 if tag != 'equal':
251 if tag != 'equal':
245 if oldfrag:
252 if oldfrag:
246 oldfrag = '<del>%s</del>' % oldfrag
253 oldfrag = '<del>%s</del>' % oldfrag
247 if newfrag:
254 if newfrag:
248 newfrag = '<ins>%s</ins>' % newfrag
255 newfrag = '<ins>%s</ins>' % newfrag
249 oldfragments.append(oldfrag)
256 oldfragments.append(oldfrag)
250 newfragments.append(newfrag)
257 newfragments.append(newfrag)
251
258
252 old['line'] = "".join(oldfragments)
259 old['line'] = "".join(oldfragments)
253 new['line'] = "".join(newfragments)
260 new['line'] = "".join(newfragments)
254
261
255 def _highlight_line_udiff(self, line, next_):
262 def _highlight_line_udiff(self, line, next_):
256 """
263 """
257 Highlight inline changes in both lines.
264 Highlight inline changes in both lines.
258 """
265 """
259 start = 0
266 start = 0
260 limit = min(len(line['line']), len(next_['line']))
267 limit = min(len(line['line']), len(next_['line']))
261 while start < limit and line['line'][start] == next_['line'][start]:
268 while start < limit and line['line'][start] == next_['line'][start]:
262 start += 1
269 start += 1
263 end = -1
270 end = -1
264 limit -= start
271 limit -= start
265 while -end <= limit and line['line'][end] == next_['line'][end]:
272 while -end <= limit and line['line'][end] == next_['line'][end]:
266 end -= 1
273 end -= 1
267 end += 1
274 end += 1
268 if start or end:
275 if start or end:
269 def do(l):
276 def do(l):
270 last = end + len(l['line'])
277 last = end + len(l['line'])
271 if l['action'] == 'add':
278 if l['action'] == 'add':
272 tag = 'ins'
279 tag = 'ins'
273 else:
280 else:
274 tag = 'del'
281 tag = 'del'
275 l['line'] = '%s<%s>%s</%s>%s' % (
282 l['line'] = '%s<%s>%s</%s>%s' % (
276 l['line'][:start],
283 l['line'][:start],
277 tag,
284 tag,
278 l['line'][start:last],
285 l['line'][start:last],
279 tag,
286 tag,
280 l['line'][last:]
287 l['line'][last:]
281 )
288 )
282
289
283 do(line)
290 do(line)
284 do(next_)
291 do(next_)
285
292
286 def _parse_udiff(self):
293 def _parse_udiff(self):
287 """
294 """
288 Parse the diff an return data for the template.
295 Parse the diff an return data for the template.
289 """
296 """
290 lineiter = self.lines
297 lineiter = self.lines
291 files = []
298 files = []
292 try:
299 try:
293 line = lineiter.next()
300 line = lineiter.next()
294 # skip first context
301 # skip first context
295 skipfirst = True
302 skipfirst = True
296
303
297 while 1:
304 while 1:
298 # continue until we found the old file
305 # continue until we found the old file
299 if not line.startswith('--- '):
306 if not line.startswith('--- '):
300 line = lineiter.next()
307 line = lineiter.next()
301 continue
308 continue
302
309
303 chunks = []
310 chunks = []
304 stats = [0, 0]
311 stats = [0, 0]
305 operation, filename, old_rev, new_rev = \
312 operation, filename, old_rev, new_rev = \
306 self._extract_rev(line, lineiter.next())
313 self._extract_rev(line, lineiter.next())
307 files.append({
314 files.append({
308 'filename': filename,
315 'filename': filename,
309 'old_revision': old_rev,
316 'old_revision': old_rev,
310 'new_revision': new_rev,
317 'new_revision': new_rev,
311 'chunks': chunks,
318 'chunks': chunks,
312 'operation': operation,
319 'operation': operation,
313 'stats': stats,
320 'stats': stats,
314 })
321 })
315
322
316 line = lineiter.next()
323 line = lineiter.next()
317 while line:
324 while line:
318
325
319 match = self._chunk_re.match(line)
326 match = self._chunk_re.match(line)
320 if not match:
327 if not match:
321 break
328 break
322
329
323 lines = []
330 lines = []
324 chunks.append(lines)
331 chunks.append(lines)
325
332
326 old_line, old_end, new_line, new_end = \
333 old_line, old_end, new_line, new_end = \
327 [int(x or 1) for x in match.groups()[:-1]]
334 [int(x or 1) for x in match.groups()[:-1]]
328 old_line -= 1
335 old_line -= 1
329 new_line -= 1
336 new_line -= 1
330 context = len(match.groups()) == 5
337 context = len(match.groups()) == 5
331 old_end += old_line
338 old_end += old_line
332 new_end += new_line
339 new_end += new_line
333
340
334 if context:
341 if context:
335 if not skipfirst:
342 if not skipfirst:
336 lines.append({
343 lines.append({
337 'old_lineno': '...',
344 'old_lineno': '...',
338 'new_lineno': '...',
345 'new_lineno': '...',
339 'action': 'context',
346 'action': 'context',
340 'line': line,
347 'line': line,
341 })
348 })
342 else:
349 else:
343 skipfirst = False
350 skipfirst = False
344
351
345 line = lineiter.next()
352 line = lineiter.next()
346 while old_line < old_end or new_line < new_end:
353 while old_line < old_end or new_line < new_end:
347 if line:
354 if line:
348 command, line = line[0], line[1:]
355 command, line = line[0], line[1:]
349 else:
356 else:
350 command = ' '
357 command = ' '
351 affects_old = affects_new = False
358 affects_old = affects_new = False
352
359
353 # ignore those if we don't expect them
360 # ignore those if we don't expect them
354 if command in '#@':
361 if command in '#@':
355 continue
362 continue
356 elif command == '+':
363 elif command == '+':
357 affects_new = True
364 affects_new = True
358 action = 'add'
365 action = 'add'
359 stats[0] += 1
366 stats[0] += 1
360 elif command == '-':
367 elif command == '-':
361 affects_old = True
368 affects_old = True
362 action = 'del'
369 action = 'del'
363 stats[1] += 1
370 stats[1] += 1
364 else:
371 else:
365 affects_old = affects_new = True
372 affects_old = affects_new = True
366 action = 'unmod'
373 action = 'unmod'
367
374
368 old_line += affects_old
375 old_line += affects_old
369 new_line += affects_new
376 new_line += affects_new
370 lines.append({
377 lines.append({
371 'old_lineno': affects_old and old_line or '',
378 'old_lineno': affects_old and old_line or '',
372 'new_lineno': affects_new and new_line or '',
379 'new_lineno': affects_new and new_line or '',
373 'action': action,
380 'action': action,
374 'line': line
381 'line': line
375 })
382 })
376 line = lineiter.next()
383 line = lineiter.next()
377 except StopIteration:
384 except StopIteration:
378 pass
385 pass
379
386
380 # highlight inline changes
387 # highlight inline changes
381 for diff_data in files:
388 for diff_data in files:
382 for chunk in diff_data['chunks']:
389 for chunk in diff_data['chunks']:
383 lineiter = iter(chunk)
390 lineiter = iter(chunk)
384 try:
391 try:
385 while 1:
392 while 1:
386 line = lineiter.next()
393 line = lineiter.next()
387 if line['action'] != 'unmod':
394 if line['action'] != 'unmod':
388 nextline = lineiter.next()
395 nextline = lineiter.next()
389 if nextline['action'] == 'unmod' or \
396 if nextline['action'] == 'unmod' or \
390 nextline['action'] == line['action']:
397 nextline['action'] == line['action']:
391 continue
398 continue
392 self.differ(line, nextline)
399 self.differ(line, nextline)
393 except StopIteration:
400 except StopIteration:
394 pass
401 pass
395 return files
402 return files
396
403
397 def prepare(self):
404 def prepare(self):
398 """
405 """
399 Prepare the passed udiff for HTML rendering. It'l return a list
406 Prepare the passed udiff for HTML rendering. It'l return a list
400 of dicts
407 of dicts
401 """
408 """
402 return self._parse_udiff()
409 return self._parse_udiff()
403
410
404 def _safe_id(self, idstring):
411 def _safe_id(self, idstring):
405 """Make a string safe for including in an id attribute.
412 """Make a string safe for including in an id attribute.
406
413
407 The HTML spec says that id attributes 'must begin with
414 The HTML spec says that id attributes 'must begin with
408 a letter ([A-Za-z]) and may be followed by any number
415 a letter ([A-Za-z]) and may be followed by any number
409 of letters, digits ([0-9]), hyphens ("-"), underscores
416 of letters, digits ([0-9]), hyphens ("-"), underscores
410 ("_"), colons (":"), and periods (".")'. These regexps
417 ("_"), colons (":"), and periods (".")'. These regexps
411 are slightly over-zealous, in that they remove colons
418 are slightly over-zealous, in that they remove colons
412 and periods unnecessarily.
419 and periods unnecessarily.
413
420
414 Whitespace is transformed into underscores, and then
421 Whitespace is transformed into underscores, and then
415 anything which is not a hyphen or a character that
422 anything which is not a hyphen or a character that
416 matches \w (alphanumerics and underscore) is removed.
423 matches \w (alphanumerics and underscore) is removed.
417
424
418 """
425 """
419 # Transform all whitespace to underscore
426 # Transform all whitespace to underscore
420 idstring = re.sub(r'\s', "_", '%s' % idstring)
427 idstring = re.sub(r'\s', "_", '%s' % idstring)
421 # Remove everything that is not a hyphen or a member of \w
428 # Remove everything that is not a hyphen or a member of \w
422 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
429 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
423 return idstring
430 return idstring
424
431
425 def raw_diff(self):
432 def raw_diff(self):
426 """
433 """
427 Returns raw string as udiff
434 Returns raw string as udiff
428 """
435 """
429 udiff_copy = self.copy_iterator()
436 udiff_copy = self.copy_iterator()
430 if self.__format == 'gitdiff':
437 if self.__format == 'gitdiff':
431 udiff_copy = self._parse_gitdiff(udiff_copy)
438 udiff_copy = self._parse_gitdiff(udiff_copy)
432 return u''.join(udiff_copy)
439 return u''.join(udiff_copy)
433
440
434 def as_html(self, table_class='code-difftable', line_class='line',
441 def as_html(self, table_class='code-difftable', line_class='line',
435 new_lineno_class='lineno old', old_lineno_class='lineno new',
442 new_lineno_class='lineno old', old_lineno_class='lineno new',
436 code_class='code', enable_comments=False, diff_lines=None):
443 code_class='code', enable_comments=False, diff_lines=None):
437 """
444 """
438 Return given diff as html table with customized css classes
445 Return given diff as html table with customized css classes
439 """
446 """
440 def _link_to_if(condition, label, url):
447 def _link_to_if(condition, label, url):
441 """
448 """
442 Generates a link if condition is meet or just the label if not.
449 Generates a link if condition is meet or just the label if not.
443 """
450 """
444
451
445 if condition:
452 if condition:
446 return '''<a href="%(url)s">%(label)s</a>''' % {
453 return '''<a href="%(url)s">%(label)s</a>''' % {
447 'url': url,
454 'url': url,
448 'label': label
455 'label': label
449 }
456 }
450 else:
457 else:
451 return label
458 return label
452 if diff_lines is None:
459 if diff_lines is None:
453 diff_lines = self.prepare()
460 diff_lines = self.prepare()
454 _html_empty = True
461 _html_empty = True
455 _html = []
462 _html = []
456 _html.append('''<table class="%(table_class)s">\n''' % {
463 _html.append('''<table class="%(table_class)s">\n''' % {
457 'table_class': table_class
464 'table_class': table_class
458 })
465 })
459 for diff in diff_lines:
466 for diff in diff_lines:
460 for line in diff['chunks']:
467 for line in diff['chunks']:
461 _html_empty = False
468 _html_empty = False
462 for change in line:
469 for change in line:
463 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
470 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
464 'lc': line_class,
471 'lc': line_class,
465 'action': change['action']
472 'action': change['action']
466 })
473 })
467 anchor_old_id = ''
474 anchor_old_id = ''
468 anchor_new_id = ''
475 anchor_new_id = ''
469 anchor_old = "%(filename)s_o%(oldline_no)s" % {
476 anchor_old = "%(filename)s_o%(oldline_no)s" % {
470 'filename': self._safe_id(diff['filename']),
477 'filename': self._safe_id(diff['filename']),
471 'oldline_no': change['old_lineno']
478 'oldline_no': change['old_lineno']
472 }
479 }
473 anchor_new = "%(filename)s_n%(oldline_no)s" % {
480 anchor_new = "%(filename)s_n%(oldline_no)s" % {
474 'filename': self._safe_id(diff['filename']),
481 'filename': self._safe_id(diff['filename']),
475 'oldline_no': change['new_lineno']
482 'oldline_no': change['new_lineno']
476 }
483 }
477 cond_old = (change['old_lineno'] != '...' and
484 cond_old = (change['old_lineno'] != '...' and
478 change['old_lineno'])
485 change['old_lineno'])
479 cond_new = (change['new_lineno'] != '...' and
486 cond_new = (change['new_lineno'] != '...' and
480 change['new_lineno'])
487 change['new_lineno'])
481 if cond_old:
488 if cond_old:
482 anchor_old_id = 'id="%s"' % anchor_old
489 anchor_old_id = 'id="%s"' % anchor_old
483 if cond_new:
490 if cond_new:
484 anchor_new_id = 'id="%s"' % anchor_new
491 anchor_new_id = 'id="%s"' % anchor_new
485 ###########################################################
492 ###########################################################
486 # OLD LINE NUMBER
493 # OLD LINE NUMBER
487 ###########################################################
494 ###########################################################
488 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
495 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
489 'a_id': anchor_old_id,
496 'a_id': anchor_old_id,
490 'olc': old_lineno_class
497 'olc': old_lineno_class
491 })
498 })
492
499
493 _html.append('''%(link)s''' % {
500 _html.append('''%(link)s''' % {
494 'link': _link_to_if(True, change['old_lineno'],
501 'link': _link_to_if(True, change['old_lineno'],
495 '#%s' % anchor_old)
502 '#%s' % anchor_old)
496 })
503 })
497 _html.append('''</td>\n''')
504 _html.append('''</td>\n''')
498 ###########################################################
505 ###########################################################
499 # NEW LINE NUMBER
506 # NEW LINE NUMBER
500 ###########################################################
507 ###########################################################
501
508
502 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
509 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
503 'a_id': anchor_new_id,
510 'a_id': anchor_new_id,
504 'nlc': new_lineno_class
511 'nlc': new_lineno_class
505 })
512 })
506
513
507 _html.append('''%(link)s''' % {
514 _html.append('''%(link)s''' % {
508 'link': _link_to_if(True, change['new_lineno'],
515 'link': _link_to_if(True, change['new_lineno'],
509 '#%s' % anchor_new)
516 '#%s' % anchor_new)
510 })
517 })
511 _html.append('''</td>\n''')
518 _html.append('''</td>\n''')
512 ###########################################################
519 ###########################################################
513 # CODE
520 # CODE
514 ###########################################################
521 ###########################################################
515 comments = '' if enable_comments else 'no-comment'
522 comments = '' if enable_comments else 'no-comment'
516 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
523 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
517 'cc': code_class,
524 'cc': code_class,
518 'inc': comments
525 'inc': comments
519 })
526 })
520 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
527 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
521 'code': change['line']
528 'code': change['line']
522 })
529 })
523 _html.append('''\t</td>''')
530 _html.append('''\t</td>''')
524 _html.append('''\n</tr>\n''')
531 _html.append('''\n</tr>\n''')
525 _html.append('''</table>''')
532 _html.append('''</table>''')
526 if _html_empty:
533 if _html_empty:
527 return None
534 return None
528 return ''.join(_html)
535 return ''.join(_html)
529
536
530 def stat(self):
537 def stat(self):
531 """
538 """
532 Returns tuple of added, and removed lines for this instance
539 Returns tuple of added, and removed lines for this instance
533 """
540 """
534 return self.adds, self.removes
541 return self.adds, self.removes
535
542
536
543
537 def differ(org_repo, org_ref, other_repo, other_ref):
544 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
538 """
545 """
539 General differ between branches, bookmarks or separate but releated
546 General differ between branches, bookmarks or separate but releated
540 repositories
547 repositories
541
548
542 :param org_repo:
549 :param org_repo:
543 :type org_repo:
550 :type org_repo:
544 :param org_ref:
551 :param org_ref:
545 :type org_ref:
552 :type org_ref:
546 :param other_repo:
553 :param other_repo:
547 :type other_repo:
554 :type other_repo:
548 :param other_ref:
555 :param other_ref:
549 :type other_ref:
556 :type other_ref:
550 """
557 """
558
551 ignore_whitespace = False
559 ignore_whitespace = False
552 context = 3
560 context = 3
553 from mercurial import patch
554 from mercurial.mdiff import diffopts
555
556 org_repo = org_repo.scm_instance._repo
561 org_repo = org_repo.scm_instance._repo
557 other_repo = other_repo.scm_instance._repo
562 other_repo = other_repo.scm_instance._repo
558
563 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
559 org_ref = org_ref[1]
564 org_ref = org_ref[1]
560 other_ref = other_ref[1]
565 other_ref = other_ref[1]
561
566
562 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
567 if org_repo != other_repo:
568
569 common, incoming, rheads = discovery_data
570 # create a bundle (uncompressed if other repo is not local)
571 if other_repo.capable('getbundle'):
572 # disable repo hooks here since it's just bundle !
573 # patch and reset hooks section of UI config to not run any
574 # hooks on fetching archives with subrepos
575 for k, _ in other_repo.ui.configitems('hooks'):
576 other_repo.ui.setconfig('hooks', k, None)
577
578 unbundle = other_repo.getbundle('incoming', common=common,
579 heads=rheads)
580
581 buf = io.BytesIO()
582 while True:
583 chunk = unbundle._stream.read(1024*4)
584 if not chunk:
585 break
586 buf.write(chunk)
563
587
588 buf.seek(0)
589 unbundle._stream = buf
590
591 class InMemoryBundleRepo(bundlerepository):
592 def __init__(self, ui, path, bundlestream):
593 self._tempparent = None
594 localrepo.localrepository.__init__(self, ui, path)
595 self.ui.setconfig('phases', 'publish', False)
596
597 self.bundle = bundlestream
598
599 # dict with the mapping 'filename' -> position in the bundle
600 self.bundlefilespos = {}
601
602 ui = make_ui('db')
603 bundlerepo = InMemoryBundleRepo(ui, path=other_repo.root,
604 bundlestream=unbundle)
605 return ''.join(patch.diff(bundlerepo, node1=org_ref, node2=other_ref,
606 opts=opts))
607 else:
564 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
608 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
565 opts=opts))
609 opts=opts))
General Comments 0
You need to be logged in to leave comments. Login now