##// END OF EJS Templates
added line context control to diffs
marcink -
r1768:5610fd9b beta
parent child Browse files
Show More
@@ -1,302 +1,308 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.changeset
3 rhodecode.controllers.changeset
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 changeset controller for pylons showoing changes beetween
6 changeset controller for pylons showoing changes beetween
7 revisions
7 revisions
8
8
9 :created_on: Apr 25, 2010
9 :created_on: Apr 25, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2011 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 pylons import tmpl_context as c, url, request, response
29 from pylons import tmpl_context as c, url, request, response
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.decorators import jsonify
32 from pylons.decorators import jsonify
33
33
34 import rhodecode.lib.helpers as h
34 import rhodecode.lib.helpers as h
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.utils import EmptyChangeset
37 from rhodecode.lib.utils import EmptyChangeset
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib import diffs
39 from rhodecode.lib import diffs
40 from rhodecode.model.db import ChangesetComment
40 from rhodecode.model.db import ChangesetComment
41 from rhodecode.model.comment import ChangesetCommentsModel
41 from rhodecode.model.comment import ChangesetCommentsModel
42
42
43 from vcs.exceptions import RepositoryError, ChangesetError, \
43 from vcs.exceptions import RepositoryError, ChangesetError, \
44 ChangesetDoesNotExistError
44 ChangesetDoesNotExistError
45 from vcs.nodes import FileNode
45 from vcs.nodes import FileNode
46 from webob.exc import HTTPForbidden
46 from webob.exc import HTTPForbidden
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class ChangesetController(BaseRepoController):
52 class ChangesetController(BaseRepoController):
53
53
54 @LoginRequired()
54 @LoginRequired()
55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
56 'repository.admin')
56 'repository.admin')
57 def __before__(self):
57 def __before__(self):
58 super(ChangesetController, self).__before__()
58 super(ChangesetController, self).__before__()
59 c.affected_files_cut_off = 60
59 c.affected_files_cut_off = 60
60
60
61 def index(self, revision):
61 def index(self, revision):
62 ignore_whitespace = request.GET.get('ignorews') == '1'
62 ignore_whitespace = request.GET.get('ignorews') == '1'
63 line_context = request.GET.get('context', 3)
63 def wrap_to_table(str):
64 def wrap_to_table(str):
64
65
65 return '''<table class="code-difftable">
66 return '''<table class="code-difftable">
66 <tr class="line">
67 <tr class="line">
67 <td class="lineno new"></td>
68 <td class="lineno new"></td>
68 <td class="code"><pre>%s</pre></td>
69 <td class="code"><pre>%s</pre></td>
69 </tr>
70 </tr>
70 </table>''' % str
71 </table>''' % str
71
72
72 #get ranges of revisions if preset
73 #get ranges of revisions if preset
73 rev_range = revision.split('...')[:2]
74 rev_range = revision.split('...')[:2]
74
75
75 try:
76 try:
76 if len(rev_range) == 2:
77 if len(rev_range) == 2:
77 rev_start = rev_range[0]
78 rev_start = rev_range[0]
78 rev_end = rev_range[1]
79 rev_end = rev_range[1]
79 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
80 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
80 end=rev_end)
81 end=rev_end)
81 else:
82 else:
82 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
83 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
83
84
84 c.cs_ranges = list(rev_ranges)
85 c.cs_ranges = list(rev_ranges)
85 if not c.cs_ranges:
86 if not c.cs_ranges:
86 raise RepositoryError('Changeset range returned empty result')
87 raise RepositoryError('Changeset range returned empty result')
87
88
88 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 log.error(traceback.format_exc())
90 log.error(traceback.format_exc())
90 h.flash(str(e), category='warning')
91 h.flash(str(e), category='warning')
91 return redirect(url('home'))
92 return redirect(url('home'))
92
93
93 c.changes = OrderedDict()
94 c.changes = OrderedDict()
94 c.sum_added = 0
95 c.sum_added = 0
95 c.sum_removed = 0
96 c.sum_removed = 0
96 c.lines_added = 0
97 c.lines_added = 0
97 c.lines_deleted = 0
98 c.lines_deleted = 0
98 c.cut_off = False # defines if cut off limit is reached
99 c.cut_off = False # defines if cut off limit is reached
99
100
100 c.comments = []
101 c.comments = []
101 c.inline_comments = []
102 c.inline_comments = []
102 c.inline_cnt = 0
103 c.inline_cnt = 0
103 # Iterate over ranges (default changeset view is always one changeset)
104 # Iterate over ranges (default changeset view is always one changeset)
104 for changeset in c.cs_ranges:
105 for changeset in c.cs_ranges:
105 c.comments.extend(ChangesetCommentsModel()\
106 c.comments.extend(ChangesetCommentsModel()\
106 .get_comments(c.rhodecode_db_repo.repo_id,
107 .get_comments(c.rhodecode_db_repo.repo_id,
107 changeset.raw_id))
108 changeset.raw_id))
108 inlines = ChangesetCommentsModel()\
109 inlines = ChangesetCommentsModel()\
109 .get_inline_comments(c.rhodecode_db_repo.repo_id,
110 .get_inline_comments(c.rhodecode_db_repo.repo_id,
110 changeset.raw_id)
111 changeset.raw_id)
111 c.inline_comments.extend(inlines)
112 c.inline_comments.extend(inlines)
112 c.changes[changeset.raw_id] = []
113 c.changes[changeset.raw_id] = []
113 try:
114 try:
114 changeset_parent = changeset.parents[0]
115 changeset_parent = changeset.parents[0]
115 except IndexError:
116 except IndexError:
116 changeset_parent = None
117 changeset_parent = None
117
118
118 #==================================================================
119 #==================================================================
119 # ADDED FILES
120 # ADDED FILES
120 #==================================================================
121 #==================================================================
121 for node in changeset.added:
122 for node in changeset.added:
122
123
123 filenode_old = FileNode(node.path, '', EmptyChangeset())
124 filenode_old = FileNode(node.path, '', EmptyChangeset())
124 if filenode_old.is_binary or node.is_binary:
125 if filenode_old.is_binary or node.is_binary:
125 diff = wrap_to_table(_('binary file'))
126 diff = wrap_to_table(_('binary file'))
126 st = (0, 0)
127 st = (0, 0)
127 else:
128 else:
128 # in this case node.size is good parameter since those are
129 # in this case node.size is good parameter since those are
129 # added nodes and their size defines how many changes were
130 # added nodes and their size defines how many changes were
130 # made
131 # made
131 c.sum_added += node.size
132 c.sum_added += node.size
132 if c.sum_added < self.cut_off_limit:
133 if c.sum_added < self.cut_off_limit:
133 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
134 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
134 ignore_whitespace=ignore_whitespace)
135 ignore_whitespace=ignore_whitespace,
136 context=line_context)
135 d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
137 d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
136
138
137 st = d.stat()
139 st = d.stat()
138 diff = d.as_html()
140 diff = d.as_html()
139
141
140 else:
142 else:
141 diff = wrap_to_table(_('Changeset is to big and '
143 diff = wrap_to_table(_('Changeset is to big and '
142 'was cut off, see raw '
144 'was cut off, see raw '
143 'changeset instead'))
145 'changeset instead'))
144 c.cut_off = True
146 c.cut_off = True
145 break
147 break
146
148
147 cs1 = None
149 cs1 = None
148 cs2 = node.last_changeset.raw_id
150 cs2 = node.last_changeset.raw_id
149 c.lines_added += st[0]
151 c.lines_added += st[0]
150 c.lines_deleted += st[1]
152 c.lines_deleted += st[1]
151 c.changes[changeset.raw_id].append(('added', node, diff,
153 c.changes[changeset.raw_id].append(('added', node, diff,
152 cs1, cs2, st))
154 cs1, cs2, st))
153
155
154 #==================================================================
156 #==================================================================
155 # CHANGED FILES
157 # CHANGED FILES
156 #==================================================================
158 #==================================================================
157 if not c.cut_off:
159 if not c.cut_off:
158 for node in changeset.changed:
160 for node in changeset.changed:
159 try:
161 try:
160 filenode_old = changeset_parent.get_node(node.path)
162 filenode_old = changeset_parent.get_node(node.path)
161 except ChangesetError:
163 except ChangesetError:
162 log.warning('Unable to fetch parent node for diff')
164 log.warning('Unable to fetch parent node for diff')
163 filenode_old = FileNode(node.path, '',
165 filenode_old = FileNode(node.path, '',
164 EmptyChangeset())
166 EmptyChangeset())
165
167
166 if filenode_old.is_binary or node.is_binary:
168 if filenode_old.is_binary or node.is_binary:
167 diff = wrap_to_table(_('binary file'))
169 diff = wrap_to_table(_('binary file'))
168 st = (0, 0)
170 st = (0, 0)
169 else:
171 else:
170
172
171 if c.sum_removed < self.cut_off_limit:
173 if c.sum_removed < self.cut_off_limit:
172 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
174 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
173 ignore_whitespace=ignore_whitespace)
175 ignore_whitespace=ignore_whitespace,
176 context=line_context)
174 d = diffs.DiffProcessor(f_gitdiff,
177 d = diffs.DiffProcessor(f_gitdiff,
175 format='gitdiff')
178 format='gitdiff')
176 st = d.stat()
179 st = d.stat()
177 if (st[0] + st[1]) * 256 > self.cut_off_limit:
180 if (st[0] + st[1]) * 256 > self.cut_off_limit:
178 diff = wrap_to_table(_('Diff is to big '
181 diff = wrap_to_table(_('Diff is to big '
179 'and was cut off, see '
182 'and was cut off, see '
180 'raw diff instead'))
183 'raw diff instead'))
181 else:
184 else:
182 diff = d.as_html()
185 diff = d.as_html()
183
186
184 if diff:
187 if diff:
185 c.sum_removed += len(diff)
188 c.sum_removed += len(diff)
186 else:
189 else:
187 diff = wrap_to_table(_('Changeset is to big and '
190 diff = wrap_to_table(_('Changeset is to big and '
188 'was cut off, see raw '
191 'was cut off, see raw '
189 'changeset instead'))
192 'changeset instead'))
190 c.cut_off = True
193 c.cut_off = True
191 break
194 break
192
195
193 cs1 = filenode_old.last_changeset.raw_id
196 cs1 = filenode_old.last_changeset.raw_id
194 cs2 = node.last_changeset.raw_id
197 cs2 = node.last_changeset.raw_id
195 c.lines_added += st[0]
198 c.lines_added += st[0]
196 c.lines_deleted += st[1]
199 c.lines_deleted += st[1]
197 c.changes[changeset.raw_id].append(('changed', node, diff,
200 c.changes[changeset.raw_id].append(('changed', node, diff,
198 cs1, cs2, st))
201 cs1, cs2, st))
199
202
200 #==================================================================
203 #==================================================================
201 # REMOVED FILES
204 # REMOVED FILES
202 #==================================================================
205 #==================================================================
203 if not c.cut_off:
206 if not c.cut_off:
204 for node in changeset.removed:
207 for node in changeset.removed:
205 c.changes[changeset.raw_id].append(('removed', node, None,
208 c.changes[changeset.raw_id].append(('removed', node, None,
206 None, None, (0, 0)))
209 None, None, (0, 0)))
207
210
208 # count inline comments
211 # count inline comments
209 for path, lines in c.inline_comments:
212 for path, lines in c.inline_comments:
210 for comments in lines.values():
213 for comments in lines.values():
211 c.inline_cnt += len(comments)
214 c.inline_cnt += len(comments)
212
215
213 if len(c.cs_ranges) == 1:
216 if len(c.cs_ranges) == 1:
214 c.changeset = c.cs_ranges[0]
217 c.changeset = c.cs_ranges[0]
215 c.changes = c.changes[c.changeset.raw_id]
218 c.changes = c.changes[c.changeset.raw_id]
216
219
217 return render('changeset/changeset.html')
220 return render('changeset/changeset.html')
218 else:
221 else:
219 return render('changeset/changeset_range.html')
222 return render('changeset/changeset_range.html')
220
223
221 def raw_changeset(self, revision):
224 def raw_changeset(self, revision):
222
225
223 method = request.GET.get('diff', 'show')
226 method = request.GET.get('diff', 'show')
224 ignore_whitespace = request.GET.get('ignorews') == '1'
227 ignore_whitespace = request.GET.get('ignorews') == '1'
228 line_context = request.GET.get('context', 3)
225 try:
229 try:
226 c.scm_type = c.rhodecode_repo.alias
230 c.scm_type = c.rhodecode_repo.alias
227 c.changeset = c.rhodecode_repo.get_changeset(revision)
231 c.changeset = c.rhodecode_repo.get_changeset(revision)
228 except RepositoryError:
232 except RepositoryError:
229 log.error(traceback.format_exc())
233 log.error(traceback.format_exc())
230 return redirect(url('home'))
234 return redirect(url('home'))
231 else:
235 else:
232 try:
236 try:
233 c.changeset_parent = c.changeset.parents[0]
237 c.changeset_parent = c.changeset.parents[0]
234 except IndexError:
238 except IndexError:
235 c.changeset_parent = None
239 c.changeset_parent = None
236 c.changes = []
240 c.changes = []
237
241
238 for node in c.changeset.added:
242 for node in c.changeset.added:
239 filenode_old = FileNode(node.path, '')
243 filenode_old = FileNode(node.path, '')
240 if filenode_old.is_binary or node.is_binary:
244 if filenode_old.is_binary or node.is_binary:
241 diff = _('binary file') + '\n'
245 diff = _('binary file') + '\n'
242 else:
246 else:
243 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
247 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
244 ignore_whitespace=ignore_whitespace)
248 ignore_whitespace=ignore_whitespace,
249 context=line_context)
245 diff = diffs.DiffProcessor(f_gitdiff,
250 diff = diffs.DiffProcessor(f_gitdiff,
246 format='gitdiff').raw_diff()
251 format='gitdiff').raw_diff()
247
252
248 cs1 = None
253 cs1 = None
249 cs2 = node.last_changeset.raw_id
254 cs2 = node.last_changeset.raw_id
250 c.changes.append(('added', node, diff, cs1, cs2))
255 c.changes.append(('added', node, diff, cs1, cs2))
251
256
252 for node in c.changeset.changed:
257 for node in c.changeset.changed:
253 filenode_old = c.changeset_parent.get_node(node.path)
258 filenode_old = c.changeset_parent.get_node(node.path)
254 if filenode_old.is_binary or node.is_binary:
259 if filenode_old.is_binary or node.is_binary:
255 diff = _('binary file')
260 diff = _('binary file')
256 else:
261 else:
257 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
262 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
258 ignore_whitespace=ignore_whitespace)
263 ignore_whitespace=ignore_whitespace,
264 context=line_context)
259 diff = diffs.DiffProcessor(f_gitdiff,
265 diff = diffs.DiffProcessor(f_gitdiff,
260 format='gitdiff').raw_diff()
266 format='gitdiff').raw_diff()
261
267
262 cs1 = filenode_old.last_changeset.raw_id
268 cs1 = filenode_old.last_changeset.raw_id
263 cs2 = node.last_changeset.raw_id
269 cs2 = node.last_changeset.raw_id
264 c.changes.append(('changed', node, diff, cs1, cs2))
270 c.changes.append(('changed', node, diff, cs1, cs2))
265
271
266 response.content_type = 'text/plain'
272 response.content_type = 'text/plain'
267
273
268 if method == 'download':
274 if method == 'download':
269 response.content_disposition = 'attachment; filename=%s.patch' \
275 response.content_disposition = 'attachment; filename=%s.patch' \
270 % revision
276 % revision
271
277
272 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
278 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
273 c.changeset.parents])
279 c.changeset.parents])
274
280
275 c.diffs = ''
281 c.diffs = ''
276 for x in c.changes:
282 for x in c.changes:
277 c.diffs += x[2]
283 c.diffs += x[2]
278
284
279 return render('changeset/raw_changeset.html')
285 return render('changeset/raw_changeset.html')
280
286
281 def comment(self, repo_name, revision):
287 def comment(self, repo_name, revision):
282 ChangesetCommentsModel().create(text=request.POST.get('text'),
288 ChangesetCommentsModel().create(text=request.POST.get('text'),
283 repo_id=c.rhodecode_db_repo.repo_id,
289 repo_id=c.rhodecode_db_repo.repo_id,
284 user_id=c.rhodecode_user.user_id,
290 user_id=c.rhodecode_user.user_id,
285 revision=revision,
291 revision=revision,
286 f_path=request.POST.get('f_path'),
292 f_path=request.POST.get('f_path'),
287 line_no=request.POST.get('line'))
293 line_no=request.POST.get('line'))
288 Session.commit()
294 Session.commit()
289 return redirect(h.url('changeset_home', repo_name=repo_name,
295 return redirect(h.url('changeset_home', repo_name=repo_name,
290 revision=revision))
296 revision=revision))
291
297
292 @jsonify
298 @jsonify
293 def delete_comment(self, repo_name, comment_id):
299 def delete_comment(self, repo_name, comment_id):
294 co = ChangesetComment.get(comment_id)
300 co = ChangesetComment.get(comment_id)
295 owner = lambda : co.author.user_id == c.rhodecode_user.user_id
301 owner = lambda : co.author.user_id == c.rhodecode_user.user_id
296 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
302 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
297 ChangesetCommentsModel().delete(comment=co)
303 ChangesetCommentsModel().delete(comment=co)
298 Session.commit()
304 Session.commit()
299 return True
305 return True
300 else:
306 else:
301 raise HTTPForbidden()
307 raise HTTPForbidden()
302
308
@@ -1,518 +1,523 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.files
3 rhodecode.controllers.files
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Files controller for RhodeCode
6 Files controller for RhodeCode
7
7
8 :created_on: Apr 21, 2010
8 :created_on: Apr 21, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2011 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 os
26 import os
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from os.path import join as jn
30 from os.path import join as jn
31
31
32 from pylons import request, response, session, tmpl_context as c, url
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from pylons.controllers.util import redirect
34 from pylons.controllers.util import redirect
35 from pylons.decorators import jsonify
35 from pylons.decorators import jsonify
36
36
37 from vcs.conf import settings
37 from vcs.conf import settings
38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
40 from vcs.nodes import FileNode, NodeKind
40 from vcs.nodes import FileNode, NodeKind
41
41
42
42
43 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
43 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 from rhodecode.lib.base import BaseRepoController, render
45 from rhodecode.lib.base import BaseRepoController, render
46 from rhodecode.lib.utils import EmptyChangeset
46 from rhodecode.lib.utils import EmptyChangeset
47 from rhodecode.lib import diffs
47 from rhodecode.lib import diffs
48 import rhodecode.lib.helpers as h
48 import rhodecode.lib.helpers as h
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class FilesController(BaseRepoController):
54 class FilesController(BaseRepoController):
55
55
56 @LoginRequired()
56 @LoginRequired()
57 def __before__(self):
57 def __before__(self):
58 super(FilesController, self).__before__()
58 super(FilesController, self).__before__()
59 c.cut_off_limit = self.cut_off_limit
59 c.cut_off_limit = self.cut_off_limit
60
60
61 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
61 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
62 """
62 """
63 Safe way to get changeset if error occur it redirects to tip with
63 Safe way to get changeset if error occur it redirects to tip with
64 proper message
64 proper message
65
65
66 :param rev: revision to fetch
66 :param rev: revision to fetch
67 :param repo_name: repo name to redirect after
67 :param repo_name: repo name to redirect after
68 """
68 """
69
69
70 try:
70 try:
71 return c.rhodecode_repo.get_changeset(rev)
71 return c.rhodecode_repo.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 url_ = url('files_add_home',
75 url_ = url('files_add_home',
76 repo_name=c.repo_name,
76 repo_name=c.repo_name,
77 revision=0, f_path='')
77 revision=0, f_path='')
78 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
78 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
79 h.flash(h.literal(_('There are no files yet %s' % add_new)),
79 h.flash(h.literal(_('There are no files yet %s' % add_new)),
80 category='warning')
80 category='warning')
81 redirect(h.url('summary_home', repo_name=repo_name))
81 redirect(h.url('summary_home', repo_name=repo_name))
82
82
83 except RepositoryError, e:
83 except RepositoryError, e:
84 h.flash(str(e), category='warning')
84 h.flash(str(e), category='warning')
85 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
85 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
86
86
87 def __get_filenode_or_redirect(self, repo_name, cs, path):
87 def __get_filenode_or_redirect(self, repo_name, cs, path):
88 """
88 """
89 Returns file_node, if error occurs or given path is directory,
89 Returns file_node, if error occurs or given path is directory,
90 it'll redirect to top level path
90 it'll redirect to top level path
91
91
92 :param repo_name: repo_name
92 :param repo_name: repo_name
93 :param cs: given changeset
93 :param cs: given changeset
94 :param path: path to lookup
94 :param path: path to lookup
95 """
95 """
96
96
97 try:
97 try:
98 file_node = cs.get_node(path)
98 file_node = cs.get_node(path)
99 if file_node.is_dir():
99 if file_node.is_dir():
100 raise RepositoryError('given path is a directory')
100 raise RepositoryError('given path is a directory')
101 except RepositoryError, e:
101 except RepositoryError, e:
102 h.flash(str(e), category='warning')
102 h.flash(str(e), category='warning')
103 redirect(h.url('files_home', repo_name=repo_name,
103 redirect(h.url('files_home', repo_name=repo_name,
104 revision=cs.raw_id))
104 revision=cs.raw_id))
105
105
106 return file_node
106 return file_node
107
107
108
108
109 def __get_paths(self, changeset, starting_path):
109 def __get_paths(self, changeset, starting_path):
110 """recursive walk in root dir and return a set of all path in that dir
110 """recursive walk in root dir and return a set of all path in that dir
111 based on repository walk function
111 based on repository walk function
112 """
112 """
113 _files = list()
113 _files = list()
114 _dirs = list()
114 _dirs = list()
115
115
116 try:
116 try:
117 tip = changeset
117 tip = changeset
118 for topnode, dirs, files in tip.walk(starting_path):
118 for topnode, dirs, files in tip.walk(starting_path):
119 for f in files:
119 for f in files:
120 _files.append(f.path)
120 _files.append(f.path)
121 for d in dirs:
121 for d in dirs:
122 _dirs.append(d.path)
122 _dirs.append(d.path)
123 except RepositoryError, e:
123 except RepositoryError, e:
124 log.debug(traceback.format_exc())
124 log.debug(traceback.format_exc())
125 pass
125 pass
126 return _dirs, _files
126 return _dirs, _files
127
127
128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
129 'repository.admin')
129 'repository.admin')
130 def index(self, repo_name, revision, f_path):
130 def index(self, repo_name, revision, f_path):
131 #reditect to given revision from form if given
131 #reditect to given revision from form if given
132 post_revision = request.POST.get('at_rev', None)
132 post_revision = request.POST.get('at_rev', None)
133 if post_revision:
133 if post_revision:
134 cs = self.__get_cs_or_redirect(post_revision, repo_name)
134 cs = self.__get_cs_or_redirect(post_revision, repo_name)
135 redirect(url('files_home', repo_name=c.repo_name,
135 redirect(url('files_home', repo_name=c.repo_name,
136 revision=cs.raw_id, f_path=f_path))
136 revision=cs.raw_id, f_path=f_path))
137
137
138 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
138 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
139 c.branch = request.GET.get('branch', None)
139 c.branch = request.GET.get('branch', None)
140 c.f_path = f_path
140 c.f_path = f_path
141
141
142 cur_rev = c.changeset.revision
142 cur_rev = c.changeset.revision
143
143
144 #prev link
144 #prev link
145 try:
145 try:
146 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
146 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
147 c.url_prev = url('files_home', repo_name=c.repo_name,
147 c.url_prev = url('files_home', repo_name=c.repo_name,
148 revision=prev_rev.raw_id, f_path=f_path)
148 revision=prev_rev.raw_id, f_path=f_path)
149 if c.branch:
149 if c.branch:
150 c.url_prev += '?branch=%s' % c.branch
150 c.url_prev += '?branch=%s' % c.branch
151 except (ChangesetDoesNotExistError, VCSError):
151 except (ChangesetDoesNotExistError, VCSError):
152 c.url_prev = '#'
152 c.url_prev = '#'
153
153
154 #next link
154 #next link
155 try:
155 try:
156 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
156 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
157 c.url_next = url('files_home', repo_name=c.repo_name,
157 c.url_next = url('files_home', repo_name=c.repo_name,
158 revision=next_rev.raw_id, f_path=f_path)
158 revision=next_rev.raw_id, f_path=f_path)
159 if c.branch:
159 if c.branch:
160 c.url_next += '?branch=%s' % c.branch
160 c.url_next += '?branch=%s' % c.branch
161 except (ChangesetDoesNotExistError, VCSError):
161 except (ChangesetDoesNotExistError, VCSError):
162 c.url_next = '#'
162 c.url_next = '#'
163
163
164 #files or dirs
164 #files or dirs
165 try:
165 try:
166 c.file = c.changeset.get_node(f_path)
166 c.file = c.changeset.get_node(f_path)
167
167
168 if c.file.is_file():
168 if c.file.is_file():
169 c.file_history = self._get_node_history(c.changeset, f_path)
169 c.file_history = self._get_node_history(c.changeset, f_path)
170 else:
170 else:
171 c.file_history = []
171 c.file_history = []
172 except RepositoryError, e:
172 except RepositoryError, e:
173 h.flash(str(e), category='warning')
173 h.flash(str(e), category='warning')
174 redirect(h.url('files_home', repo_name=repo_name,
174 redirect(h.url('files_home', repo_name=repo_name,
175 revision=revision))
175 revision=revision))
176
176
177 return render('files/files.html')
177 return render('files/files.html')
178
178
179 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
179 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 'repository.admin')
180 'repository.admin')
181 def rawfile(self, repo_name, revision, f_path):
181 def rawfile(self, repo_name, revision, f_path):
182 cs = self.__get_cs_or_redirect(revision, repo_name)
182 cs = self.__get_cs_or_redirect(revision, repo_name)
183 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
183 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
184
184
185 response.content_disposition = 'attachment; filename=%s' % \
185 response.content_disposition = 'attachment; filename=%s' % \
186 safe_str(f_path.split(os.sep)[-1])
186 safe_str(f_path.split(os.sep)[-1])
187
187
188 response.content_type = file_node.mimetype
188 response.content_type = file_node.mimetype
189 return file_node.content
189 return file_node.content
190
190
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 'repository.admin')
192 'repository.admin')
193 def raw(self, repo_name, revision, f_path):
193 def raw(self, repo_name, revision, f_path):
194 cs = self.__get_cs_or_redirect(revision, repo_name)
194 cs = self.__get_cs_or_redirect(revision, repo_name)
195 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
195 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
196
196
197 raw_mimetype_mapping = {
197 raw_mimetype_mapping = {
198 # map original mimetype to a mimetype used for "show as raw"
198 # map original mimetype to a mimetype used for "show as raw"
199 # you can also provide a content-disposition to override the
199 # you can also provide a content-disposition to override the
200 # default "attachment" disposition.
200 # default "attachment" disposition.
201 # orig_type: (new_type, new_dispo)
201 # orig_type: (new_type, new_dispo)
202
202
203 # show images inline:
203 # show images inline:
204 'image/x-icon': ('image/x-icon', 'inline'),
204 'image/x-icon': ('image/x-icon', 'inline'),
205 'image/png': ('image/png', 'inline'),
205 'image/png': ('image/png', 'inline'),
206 'image/gif': ('image/gif', 'inline'),
206 'image/gif': ('image/gif', 'inline'),
207 'image/jpeg': ('image/jpeg', 'inline'),
207 'image/jpeg': ('image/jpeg', 'inline'),
208 'image/svg+xml': ('image/svg+xml', 'inline'),
208 'image/svg+xml': ('image/svg+xml', 'inline'),
209 }
209 }
210
210
211 mimetype = file_node.mimetype
211 mimetype = file_node.mimetype
212 try:
212 try:
213 mimetype, dispo = raw_mimetype_mapping[mimetype]
213 mimetype, dispo = raw_mimetype_mapping[mimetype]
214 except KeyError:
214 except KeyError:
215 # we don't know anything special about this, handle it safely
215 # we don't know anything special about this, handle it safely
216 if file_node.is_binary:
216 if file_node.is_binary:
217 # do same as download raw for binary files
217 # do same as download raw for binary files
218 mimetype, dispo = 'application/octet-stream', 'attachment'
218 mimetype, dispo = 'application/octet-stream', 'attachment'
219 else:
219 else:
220 # do not just use the original mimetype, but force text/plain,
220 # do not just use the original mimetype, but force text/plain,
221 # otherwise it would serve text/html and that might be unsafe.
221 # otherwise it would serve text/html and that might be unsafe.
222 # Note: underlying vcs library fakes text/plain mimetype if the
222 # Note: underlying vcs library fakes text/plain mimetype if the
223 # mimetype can not be determined and it thinks it is not
223 # mimetype can not be determined and it thinks it is not
224 # binary.This might lead to erroneous text display in some
224 # binary.This might lead to erroneous text display in some
225 # cases, but helps in other cases, like with text files
225 # cases, but helps in other cases, like with text files
226 # without extension.
226 # without extension.
227 mimetype, dispo = 'text/plain', 'inline'
227 mimetype, dispo = 'text/plain', 'inline'
228
228
229 if dispo == 'attachment':
229 if dispo == 'attachment':
230 dispo = 'attachment; filename=%s' % \
230 dispo = 'attachment; filename=%s' % \
231 safe_str(f_path.split(os.sep)[-1])
231 safe_str(f_path.split(os.sep)[-1])
232
232
233 response.content_disposition = dispo
233 response.content_disposition = dispo
234 response.content_type = mimetype
234 response.content_type = mimetype
235 return file_node.content
235 return file_node.content
236
236
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 'repository.admin')
238 'repository.admin')
239 def annotate(self, repo_name, revision, f_path):
239 def annotate(self, repo_name, revision, f_path):
240 c.cs = self.__get_cs_or_redirect(revision, repo_name)
240 c.cs = self.__get_cs_or_redirect(revision, repo_name)
241 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
241 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
242
242
243 c.file_history = self._get_node_history(c.cs, f_path)
243 c.file_history = self._get_node_history(c.cs, f_path)
244 c.f_path = f_path
244 c.f_path = f_path
245 return render('files/files_annotate.html')
245 return render('files/files_annotate.html')
246
246
247 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
247 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
248 def edit(self, repo_name, revision, f_path):
248 def edit(self, repo_name, revision, f_path):
249 r_post = request.POST
249 r_post = request.POST
250
250
251 c.cs = self.__get_cs_or_redirect(revision, repo_name)
251 c.cs = self.__get_cs_or_redirect(revision, repo_name)
252 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
252 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
253
253
254 if c.file.is_binary:
254 if c.file.is_binary:
255 return redirect(url('files_home', repo_name=c.repo_name,
255 return redirect(url('files_home', repo_name=c.repo_name,
256 revision=c.cs.raw_id, f_path=f_path))
256 revision=c.cs.raw_id, f_path=f_path))
257
257
258 c.f_path = f_path
258 c.f_path = f_path
259
259
260 if r_post:
260 if r_post:
261
261
262 old_content = c.file.content
262 old_content = c.file.content
263 sl = old_content.splitlines(1)
263 sl = old_content.splitlines(1)
264 first_line = sl[0] if sl else ''
264 first_line = sl[0] if sl else ''
265 # modes: 0 - Unix, 1 - Mac, 2 - DOS
265 # modes: 0 - Unix, 1 - Mac, 2 - DOS
266 mode = detect_mode(first_line, 0)
266 mode = detect_mode(first_line, 0)
267 content = convert_line_endings(r_post.get('content'), mode)
267 content = convert_line_endings(r_post.get('content'), mode)
268
268
269 message = r_post.get('message') or (_('Edited %s via RhodeCode')
269 message = r_post.get('message') or (_('Edited %s via RhodeCode')
270 % (f_path))
270 % (f_path))
271 author = self.rhodecode_user.full_contact
271 author = self.rhodecode_user.full_contact
272
272
273 if content == old_content:
273 if content == old_content:
274 h.flash(_('No changes'),
274 h.flash(_('No changes'),
275 category='warning')
275 category='warning')
276 return redirect(url('changeset_home', repo_name=c.repo_name,
276 return redirect(url('changeset_home', repo_name=c.repo_name,
277 revision='tip'))
277 revision='tip'))
278
278
279 try:
279 try:
280 self.scm_model.commit_change(repo=c.rhodecode_repo,
280 self.scm_model.commit_change(repo=c.rhodecode_repo,
281 repo_name=repo_name, cs=c.cs,
281 repo_name=repo_name, cs=c.cs,
282 user=self.rhodecode_user,
282 user=self.rhodecode_user,
283 author=author, message=message,
283 author=author, message=message,
284 content=content, f_path=f_path)
284 content=content, f_path=f_path)
285 h.flash(_('Successfully committed to %s' % f_path),
285 h.flash(_('Successfully committed to %s' % f_path),
286 category='success')
286 category='success')
287
287
288 except Exception:
288 except Exception:
289 log.error(traceback.format_exc())
289 log.error(traceback.format_exc())
290 h.flash(_('Error occurred during commit'), category='error')
290 h.flash(_('Error occurred during commit'), category='error')
291 return redirect(url('changeset_home',
291 return redirect(url('changeset_home',
292 repo_name=c.repo_name, revision='tip'))
292 repo_name=c.repo_name, revision='tip'))
293
293
294 return render('files/files_edit.html')
294 return render('files/files_edit.html')
295
295
296 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
296 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
297 def add(self, repo_name, revision, f_path):
297 def add(self, repo_name, revision, f_path):
298 r_post = request.POST
298 r_post = request.POST
299 c.cs = self.__get_cs_or_redirect(revision, repo_name,
299 c.cs = self.__get_cs_or_redirect(revision, repo_name,
300 redirect_after=False)
300 redirect_after=False)
301 if c.cs is None:
301 if c.cs is None:
302 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
302 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
303
303
304 c.f_path = f_path
304 c.f_path = f_path
305
305
306 if r_post:
306 if r_post:
307 unix_mode = 0
307 unix_mode = 0
308 content = convert_line_endings(r_post.get('content'), unix_mode)
308 content = convert_line_endings(r_post.get('content'), unix_mode)
309
309
310 message = r_post.get('message') or (_('Added %s via RhodeCode')
310 message = r_post.get('message') or (_('Added %s via RhodeCode')
311 % (f_path))
311 % (f_path))
312 location = r_post.get('location')
312 location = r_post.get('location')
313 filename = r_post.get('filename')
313 filename = r_post.get('filename')
314 file_obj = r_post.get('upload_file', None)
314 file_obj = r_post.get('upload_file', None)
315
315
316 if file_obj is not None and hasattr(file_obj, 'filename'):
316 if file_obj is not None and hasattr(file_obj, 'filename'):
317 filename = file_obj.filename
317 filename = file_obj.filename
318 content = file_obj.file
318 content = file_obj.file
319
319
320 node_path = os.path.join(location, filename)
320 node_path = os.path.join(location, filename)
321 author = self.rhodecode_user.full_contact
321 author = self.rhodecode_user.full_contact
322
322
323 if not content:
323 if not content:
324 h.flash(_('No content'), category='warning')
324 h.flash(_('No content'), category='warning')
325 return redirect(url('changeset_home', repo_name=c.repo_name,
325 return redirect(url('changeset_home', repo_name=c.repo_name,
326 revision='tip'))
326 revision='tip'))
327 if not filename:
327 if not filename:
328 h.flash(_('No filename'), category='warning')
328 h.flash(_('No filename'), category='warning')
329 return redirect(url('changeset_home', repo_name=c.repo_name,
329 return redirect(url('changeset_home', repo_name=c.repo_name,
330 revision='tip'))
330 revision='tip'))
331
331
332 try:
332 try:
333 self.scm_model.create_node(repo=c.rhodecode_repo,
333 self.scm_model.create_node(repo=c.rhodecode_repo,
334 repo_name=repo_name, cs=c.cs,
334 repo_name=repo_name, cs=c.cs,
335 user=self.rhodecode_user,
335 user=self.rhodecode_user,
336 author=author, message=message,
336 author=author, message=message,
337 content=content, f_path=node_path)
337 content=content, f_path=node_path)
338 h.flash(_('Successfully committed to %s' % node_path),
338 h.flash(_('Successfully committed to %s' % node_path),
339 category='success')
339 category='success')
340 except NodeAlreadyExistsError, e:
340 except NodeAlreadyExistsError, e:
341 h.flash(_(e), category='error')
341 h.flash(_(e), category='error')
342 except Exception:
342 except Exception:
343 log.error(traceback.format_exc())
343 log.error(traceback.format_exc())
344 h.flash(_('Error occurred during commit'), category='error')
344 h.flash(_('Error occurred during commit'), category='error')
345 return redirect(url('changeset_home',
345 return redirect(url('changeset_home',
346 repo_name=c.repo_name, revision='tip'))
346 repo_name=c.repo_name, revision='tip'))
347
347
348 return render('files/files_add.html')
348 return render('files/files_add.html')
349
349
350 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
351 'repository.admin')
351 'repository.admin')
352 def archivefile(self, repo_name, fname):
352 def archivefile(self, repo_name, fname):
353
353
354 fileformat = None
354 fileformat = None
355 revision = None
355 revision = None
356 ext = None
356 ext = None
357 subrepos = request.GET.get('subrepos') == 'true'
357 subrepos = request.GET.get('subrepos') == 'true'
358
358
359 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
359 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
360 archive_spec = fname.split(ext_data[1])
360 archive_spec = fname.split(ext_data[1])
361 if len(archive_spec) == 2 and archive_spec[1] == '':
361 if len(archive_spec) == 2 and archive_spec[1] == '':
362 fileformat = a_type or ext_data[1]
362 fileformat = a_type or ext_data[1]
363 revision = archive_spec[0]
363 revision = archive_spec[0]
364 ext = ext_data[1]
364 ext = ext_data[1]
365
365
366 try:
366 try:
367 dbrepo = RepoModel().get_by_repo_name(repo_name)
367 dbrepo = RepoModel().get_by_repo_name(repo_name)
368 if dbrepo.enable_downloads is False:
368 if dbrepo.enable_downloads is False:
369 return _('downloads disabled')
369 return _('downloads disabled')
370
370
371 # patch and reset hooks section of UI config to not run any
371 # patch and reset hooks section of UI config to not run any
372 # hooks on fetching archives with subrepos
372 # hooks on fetching archives with subrepos
373 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
373 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
374 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
374 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
375
375
376 cs = c.rhodecode_repo.get_changeset(revision)
376 cs = c.rhodecode_repo.get_changeset(revision)
377 content_type = settings.ARCHIVE_SPECS[fileformat][0]
377 content_type = settings.ARCHIVE_SPECS[fileformat][0]
378 except ChangesetDoesNotExistError:
378 except ChangesetDoesNotExistError:
379 return _('Unknown revision %s') % revision
379 return _('Unknown revision %s') % revision
380 except EmptyRepositoryError:
380 except EmptyRepositoryError:
381 return _('Empty repository')
381 return _('Empty repository')
382 except (ImproperArchiveTypeError, KeyError):
382 except (ImproperArchiveTypeError, KeyError):
383 return _('Unknown archive type')
383 return _('Unknown archive type')
384
384
385 response.content_type = content_type
385 response.content_type = content_type
386 response.content_disposition = 'attachment; filename=%s-%s%s' \
386 response.content_disposition = 'attachment; filename=%s-%s%s' \
387 % (repo_name, revision, ext)
387 % (repo_name, revision, ext)
388
388
389 import tempfile
389 import tempfile
390 archive = tempfile.mkstemp()[1]
390 archive = tempfile.mkstemp()[1]
391 t = open(archive, 'wb')
391 t = open(archive, 'wb')
392 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
392 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
393
393
394 def get_chunked_archive(archive):
394 def get_chunked_archive(archive):
395 stream = open(archive, 'rb')
395 stream = open(archive, 'rb')
396 while True:
396 while True:
397 data = stream.read(4096)
397 data = stream.read(4096)
398 if not data:
398 if not data:
399 os.remove(archive)
399 os.remove(archive)
400 break
400 break
401 yield data
401 yield data
402
402
403 return get_chunked_archive(archive)
403 return get_chunked_archive(archive)
404
404
405 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
405 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 'repository.admin')
406 'repository.admin')
407 def diff(self, repo_name, f_path):
407 def diff(self, repo_name, f_path):
408 ignore_whitespace = request.GET.get('ignorews') == '1'
408 ignore_whitespace = request.GET.get('ignorews') == '1'
409 line_context = request.GET.get('context', 3)
409 diff1 = request.GET.get('diff1')
410 diff1 = request.GET.get('diff1')
410 diff2 = request.GET.get('diff2')
411 diff2 = request.GET.get('diff2')
411 c.action = request.GET.get('diff')
412 c.action = request.GET.get('diff')
412 c.no_changes = diff1 == diff2
413 c.no_changes = diff1 == diff2
413 c.f_path = f_path
414 c.f_path = f_path
414 c.big_diff = False
415 c.big_diff = False
415
416
416 try:
417 try:
417 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
418 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
418 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
419 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
419 node1 = c.changeset_1.get_node(f_path)
420 node1 = c.changeset_1.get_node(f_path)
420 else:
421 else:
421 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
422 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
422 node1 = FileNode('.', '', changeset=c.changeset_1)
423 node1 = FileNode('.', '', changeset=c.changeset_1)
423
424
424 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
425 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
425 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
426 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
426 node2 = c.changeset_2.get_node(f_path)
427 node2 = c.changeset_2.get_node(f_path)
427 else:
428 else:
428 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
429 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
429 node2 = FileNode('.', '', changeset=c.changeset_2)
430 node2 = FileNode('.', '', changeset=c.changeset_2)
430 except RepositoryError:
431 except RepositoryError:
431 return redirect(url('files_home',
432 return redirect(url('files_home',
432 repo_name=c.repo_name, f_path=f_path))
433 repo_name=c.repo_name, f_path=f_path))
433
434
434 if c.action == 'download':
435 if c.action == 'download':
435 _diff = diffs.get_gitdiff(node1, node2,
436 _diff = diffs.get_gitdiff(node1, node2,
436 ignore_whitespace=ignore_whitespace)
437 ignore_whitespace=ignore_whitespace,
438 context=line_context)
437 diff = diffs.DiffProcessor(_diff,format='gitdiff')
439 diff = diffs.DiffProcessor(_diff,format='gitdiff')
438
440
439 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
441 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
440 response.content_type = 'text/plain'
442 response.content_type = 'text/plain'
441 response.content_disposition = 'attachment; filename=%s' \
443 response.content_disposition = 'attachment; filename=%s' \
442 % diff_name
444 % diff_name
443 return diff.raw_diff()
445 return diff.raw_diff()
444
446
445 elif c.action == 'raw':
447 elif c.action == 'raw':
446 _diff = diffs.get_gitdiff(node1, node2,
448 _diff = diffs.get_gitdiff(node1, node2,
447 ignore_whitespace=ignore_whitespace)
449 ignore_whitespace=ignore_whitespace,
450 context=line_context)
448 diff = diffs.DiffProcessor(_diff,format='gitdiff')
451 diff = diffs.DiffProcessor(_diff,format='gitdiff')
449 response.content_type = 'text/plain'
452 response.content_type = 'text/plain'
450 return diff.raw_diff()
453 return diff.raw_diff()
451
454
452 elif c.action == 'diff':
455 elif c.action == 'diff':
453 if node1.is_binary or node2.is_binary:
456 if node1.is_binary or node2.is_binary:
454 c.cur_diff = _('Binary file')
457 c.cur_diff = _('Binary file')
455 elif node1.size > self.cut_off_limit or \
458 elif node1.size > self.cut_off_limit or \
456 node2.size > self.cut_off_limit:
459 node2.size > self.cut_off_limit:
457 c.cur_diff = ''
460 c.cur_diff = ''
458 c.big_diff = True
461 c.big_diff = True
459 else:
462 else:
460 _diff = diffs.get_gitdiff(node1, node2,
463 _diff = diffs.get_gitdiff(node1, node2,
461 ignore_whitespace=ignore_whitespace)
464 ignore_whitespace=ignore_whitespace,
465 context=line_context)
462 diff = diffs.DiffProcessor(_diff,format='gitdiff')
466 diff = diffs.DiffProcessor(_diff,format='gitdiff')
463 c.cur_diff = diff.as_html()
467 c.cur_diff = diff.as_html()
464 else:
468 else:
465
469
466 #default option
470 #default option
467 if node1.is_binary or node2.is_binary:
471 if node1.is_binary or node2.is_binary:
468 c.cur_diff = _('Binary file')
472 c.cur_diff = _('Binary file')
469 elif node1.size > self.cut_off_limit or \
473 elif node1.size > self.cut_off_limit or \
470 node2.size > self.cut_off_limit:
474 node2.size > self.cut_off_limit:
471 c.cur_diff = ''
475 c.cur_diff = ''
472 c.big_diff = True
476 c.big_diff = True
473
477
474 else:
478 else:
475 _diff = diffs.get_gitdiff(node1, node2,
479 _diff = diffs.get_gitdiff(node1, node2,
476 ignore_whitespace=ignore_whitespace)
480 ignore_whitespace=ignore_whitespace,
481 context=line_context)
477 diff = diffs.DiffProcessor(_diff,format='gitdiff')
482 diff = diffs.DiffProcessor(_diff,format='gitdiff')
478 c.cur_diff = diff.as_html()
483 c.cur_diff = diff.as_html()
479
484
480 if not c.cur_diff and not c.big_diff:
485 if not c.cur_diff and not c.big_diff:
481 c.no_changes = True
486 c.no_changes = True
482 return render('files/file_diff.html')
487 return render('files/file_diff.html')
483
488
484 def _get_node_history(self, cs, f_path):
489 def _get_node_history(self, cs, f_path):
485 changesets = cs.get_file_history(f_path)
490 changesets = cs.get_file_history(f_path)
486 hist_l = []
491 hist_l = []
487
492
488 changesets_group = ([], _("Changesets"))
493 changesets_group = ([], _("Changesets"))
489 branches_group = ([], _("Branches"))
494 branches_group = ([], _("Branches"))
490 tags_group = ([], _("Tags"))
495 tags_group = ([], _("Tags"))
491
496
492 for chs in changesets:
497 for chs in changesets:
493 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
498 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
494 changesets_group[0].append((chs.raw_id, n_desc,))
499 changesets_group[0].append((chs.raw_id, n_desc,))
495
500
496 hist_l.append(changesets_group)
501 hist_l.append(changesets_group)
497
502
498 for name, chs in c.rhodecode_repo.branches.items():
503 for name, chs in c.rhodecode_repo.branches.items():
499 #chs = chs.split(':')[-1]
504 #chs = chs.split(':')[-1]
500 branches_group[0].append((chs, name),)
505 branches_group[0].append((chs, name),)
501 hist_l.append(branches_group)
506 hist_l.append(branches_group)
502
507
503 for name, chs in c.rhodecode_repo.tags.items():
508 for name, chs in c.rhodecode_repo.tags.items():
504 #chs = chs.split(':')[-1]
509 #chs = chs.split(':')[-1]
505 tags_group[0].append((chs, name),)
510 tags_group[0].append((chs, name),)
506 hist_l.append(tags_group)
511 hist_l.append(tags_group)
507
512
508 return hist_l
513 return hist_l
509
514
510 @jsonify
515 @jsonify
511 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
516 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
512 'repository.admin')
517 'repository.admin')
513 def nodelist(self, repo_name, revision, f_path):
518 def nodelist(self, repo_name, revision, f_path):
514 if request.environ.get('HTTP_X_PARTIAL_XHR'):
519 if request.environ.get('HTTP_X_PARTIAL_XHR'):
515 cs = self.__get_cs_or_redirect(revision, repo_name)
520 cs = self.__get_cs_or_redirect(revision, repo_name)
516 _d, _f = self.__get_paths(cs, f_path)
521 _d, _f = self.__get_paths(cs, f_path)
517 return _d + _f
522 return _d + _f
518
523
@@ -1,447 +1,447 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) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2009-2011 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
30
31 from itertools import tee, imap
31 from itertools import tee, imap
32
32
33 from mercurial.match import match
33 from mercurial.match import match
34
34
35 from vcs.exceptions import VCSError
35 from vcs.exceptions import VCSError
36 from vcs.nodes import FileNode
36 from vcs.nodes import FileNode
37
37
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
39 """
39 """
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
41
41
42 :param ignore_whitespace: ignore whitespaces in diff
42 :param ignore_whitespace: ignore whitespaces in diff
43 """
43 """
44
44
45 for filenode in (filenode_old, filenode_new):
45 for filenode in (filenode_old, filenode_new):
46 if not isinstance(filenode, FileNode):
46 if not isinstance(filenode, FileNode):
47 raise VCSError("Given object should be FileNode object, not %s"
47 raise VCSError("Given object should be FileNode object, not %s"
48 % filenode.__class__)
48 % filenode.__class__)
49
49
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
52
52
53 repo = filenode_new.changeset.repository
53 repo = filenode_new.changeset.repository
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
55 ignore_whitespace)
55 ignore_whitespace, context)
56
56
57 return vcs_gitdiff
57 return vcs_gitdiff
58
58
59
59
60 class DiffProcessor(object):
60 class DiffProcessor(object):
61 """
61 """
62 Give it a unified diff and it returns a list of the files that were
62 Give it a unified diff and it returns a list of the files that were
63 mentioned in the diff together with a dict of meta information that
63 mentioned in the diff together with a dict of meta information that
64 can be used to render it in a HTML template.
64 can be used to render it in a HTML template.
65 """
65 """
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
67
67
68 def __init__(self, diff, differ='diff', format='udiff'):
68 def __init__(self, diff, differ='diff', format='udiff'):
69 """
69 """
70 :param diff: a text in diff format or generator
70 :param diff: a text in diff format or generator
71 :param format: format of diff passed, `udiff` or `gitdiff`
71 :param format: format of diff passed, `udiff` or `gitdiff`
72 """
72 """
73 if isinstance(diff, basestring):
73 if isinstance(diff, basestring):
74 diff = [diff]
74 diff = [diff]
75
75
76 self.__udiff = diff
76 self.__udiff = diff
77 self.__format = format
77 self.__format = format
78 self.adds = 0
78 self.adds = 0
79 self.removes = 0
79 self.removes = 0
80
80
81 if isinstance(self.__udiff, basestring):
81 if isinstance(self.__udiff, basestring):
82 self.lines = iter(self.__udiff.splitlines(1))
82 self.lines = iter(self.__udiff.splitlines(1))
83
83
84 elif self.__format == 'gitdiff':
84 elif self.__format == 'gitdiff':
85 udiff_copy = self.copy_iterator()
85 udiff_copy = self.copy_iterator()
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
87 else:
87 else:
88 udiff_copy = self.copy_iterator()
88 udiff_copy = self.copy_iterator()
89 self.lines = imap(self.escaper, udiff_copy)
89 self.lines = imap(self.escaper, udiff_copy)
90
90
91 # Select a differ.
91 # Select a differ.
92 if differ == 'difflib':
92 if differ == 'difflib':
93 self.differ = self._highlight_line_difflib
93 self.differ = self._highlight_line_difflib
94 else:
94 else:
95 self.differ = self._highlight_line_udiff
95 self.differ = self._highlight_line_udiff
96
96
97 def escaper(self, string):
97 def escaper(self, string):
98 return string.replace('<', '&lt;').replace('>', '&gt;')
98 return string.replace('<', '&lt;').replace('>', '&gt;')
99
99
100 def copy_iterator(self):
100 def copy_iterator(self):
101 """
101 """
102 make a fresh copy of generator, we should not iterate thru
102 make a fresh copy of generator, we should not iterate thru
103 an original as it's needed for repeating operations on
103 an original as it's needed for repeating operations on
104 this instance of DiffProcessor
104 this instance of DiffProcessor
105 """
105 """
106 self.__udiff, iterator_copy = tee(self.__udiff)
106 self.__udiff, iterator_copy = tee(self.__udiff)
107 return iterator_copy
107 return iterator_copy
108
108
109 def _extract_rev(self, line1, line2):
109 def _extract_rev(self, line1, line2):
110 """
110 """
111 Extract the filename and revision hint from a line.
111 Extract the filename and revision hint from a line.
112 """
112 """
113
113
114 try:
114 try:
115 if line1.startswith('--- ') and line2.startswith('+++ '):
115 if line1.startswith('--- ') and line2.startswith('+++ '):
116 l1 = line1[4:].split(None, 1)
116 l1 = line1[4:].split(None, 1)
117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
118 old_rev = l1[1] if len(l1) == 2 else 'old'
118 old_rev = l1[1] if len(l1) == 2 else 'old'
119
119
120 l2 = line2[4:].split(None, 1)
120 l2 = line2[4:].split(None, 1)
121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
122 new_rev = l2[1] if len(l2) == 2 else 'new'
122 new_rev = l2[1] if len(l2) == 2 else 'new'
123
123
124 filename = old_filename if (old_filename !=
124 filename = old_filename if (old_filename !=
125 'dev/null') else new_filename
125 'dev/null') else new_filename
126
126
127 return filename, new_rev, old_rev
127 return filename, new_rev, old_rev
128 except (ValueError, IndexError):
128 except (ValueError, IndexError):
129 pass
129 pass
130
130
131 return None, None, None
131 return None, None, None
132
132
133 def _parse_gitdiff(self, diffiterator):
133 def _parse_gitdiff(self, diffiterator):
134 def line_decoder(l):
134 def line_decoder(l):
135 if l.startswith('+') and not l.startswith('+++'):
135 if l.startswith('+') and not l.startswith('+++'):
136 self.adds += 1
136 self.adds += 1
137 elif l.startswith('-') and not l.startswith('---'):
137 elif l.startswith('-') and not l.startswith('---'):
138 self.removes += 1
138 self.removes += 1
139 return l.decode('utf8', 'replace')
139 return l.decode('utf8', 'replace')
140
140
141 output = list(diffiterator)
141 output = list(diffiterator)
142 size = len(output)
142 size = len(output)
143
143
144 if size == 2:
144 if size == 2:
145 l = []
145 l = []
146 l.extend([output[0]])
146 l.extend([output[0]])
147 l.extend(output[1].splitlines(1))
147 l.extend(output[1].splitlines(1))
148 return map(line_decoder, l)
148 return map(line_decoder, l)
149 elif size == 1:
149 elif size == 1:
150 return map(line_decoder, output[0].splitlines(1))
150 return map(line_decoder, output[0].splitlines(1))
151 elif size == 0:
151 elif size == 0:
152 return []
152 return []
153
153
154 raise Exception('wrong size of diff %s' % size)
154 raise Exception('wrong size of diff %s' % size)
155
155
156 def _highlight_line_difflib(self, line, next):
156 def _highlight_line_difflib(self, line, next):
157 """
157 """
158 Highlight inline changes in both lines.
158 Highlight inline changes in both lines.
159 """
159 """
160
160
161 if line['action'] == 'del':
161 if line['action'] == 'del':
162 old, new = line, next
162 old, new = line, next
163 else:
163 else:
164 old, new = next, line
164 old, new = next, line
165
165
166 oldwords = re.split(r'(\W)', old['line'])
166 oldwords = re.split(r'(\W)', old['line'])
167 newwords = re.split(r'(\W)', new['line'])
167 newwords = re.split(r'(\W)', new['line'])
168
168
169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
170
170
171 oldfragments, newfragments = [], []
171 oldfragments, newfragments = [], []
172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
173 oldfrag = ''.join(oldwords[i1:i2])
173 oldfrag = ''.join(oldwords[i1:i2])
174 newfrag = ''.join(newwords[j1:j2])
174 newfrag = ''.join(newwords[j1:j2])
175 if tag != 'equal':
175 if tag != 'equal':
176 if oldfrag:
176 if oldfrag:
177 oldfrag = '<del>%s</del>' % oldfrag
177 oldfrag = '<del>%s</del>' % oldfrag
178 if newfrag:
178 if newfrag:
179 newfrag = '<ins>%s</ins>' % newfrag
179 newfrag = '<ins>%s</ins>' % newfrag
180 oldfragments.append(oldfrag)
180 oldfragments.append(oldfrag)
181 newfragments.append(newfrag)
181 newfragments.append(newfrag)
182
182
183 old['line'] = "".join(oldfragments)
183 old['line'] = "".join(oldfragments)
184 new['line'] = "".join(newfragments)
184 new['line'] = "".join(newfragments)
185
185
186 def _highlight_line_udiff(self, line, next):
186 def _highlight_line_udiff(self, line, next):
187 """
187 """
188 Highlight inline changes in both lines.
188 Highlight inline changes in both lines.
189 """
189 """
190 start = 0
190 start = 0
191 limit = min(len(line['line']), len(next['line']))
191 limit = min(len(line['line']), len(next['line']))
192 while start < limit and line['line'][start] == next['line'][start]:
192 while start < limit and line['line'][start] == next['line'][start]:
193 start += 1
193 start += 1
194 end = -1
194 end = -1
195 limit -= start
195 limit -= start
196 while -end <= limit and line['line'][end] == next['line'][end]:
196 while -end <= limit and line['line'][end] == next['line'][end]:
197 end -= 1
197 end -= 1
198 end += 1
198 end += 1
199 if start or end:
199 if start or end:
200 def do(l):
200 def do(l):
201 last = end + len(l['line'])
201 last = end + len(l['line'])
202 if l['action'] == 'add':
202 if l['action'] == 'add':
203 tag = 'ins'
203 tag = 'ins'
204 else:
204 else:
205 tag = 'del'
205 tag = 'del'
206 l['line'] = '%s<%s>%s</%s>%s' % (
206 l['line'] = '%s<%s>%s</%s>%s' % (
207 l['line'][:start],
207 l['line'][:start],
208 tag,
208 tag,
209 l['line'][start:last],
209 l['line'][start:last],
210 tag,
210 tag,
211 l['line'][last:]
211 l['line'][last:]
212 )
212 )
213 do(line)
213 do(line)
214 do(next)
214 do(next)
215
215
216 def _parse_udiff(self):
216 def _parse_udiff(self):
217 """
217 """
218 Parse the diff an return data for the template.
218 Parse the diff an return data for the template.
219 """
219 """
220 lineiter = self.lines
220 lineiter = self.lines
221 files = []
221 files = []
222 try:
222 try:
223 line = lineiter.next()
223 line = lineiter.next()
224 # skip first context
224 # skip first context
225 skipfirst = True
225 skipfirst = True
226 while 1:
226 while 1:
227 # continue until we found the old file
227 # continue until we found the old file
228 if not line.startswith('--- '):
228 if not line.startswith('--- '):
229 line = lineiter.next()
229 line = lineiter.next()
230 continue
230 continue
231
231
232 chunks = []
232 chunks = []
233 filename, old_rev, new_rev = \
233 filename, old_rev, new_rev = \
234 self._extract_rev(line, lineiter.next())
234 self._extract_rev(line, lineiter.next())
235 files.append({
235 files.append({
236 'filename': filename,
236 'filename': filename,
237 'old_revision': old_rev,
237 'old_revision': old_rev,
238 'new_revision': new_rev,
238 'new_revision': new_rev,
239 'chunks': chunks
239 'chunks': chunks
240 })
240 })
241
241
242 line = lineiter.next()
242 line = lineiter.next()
243 while line:
243 while line:
244 match = self._chunk_re.match(line)
244 match = self._chunk_re.match(line)
245 if not match:
245 if not match:
246 break
246 break
247
247
248 lines = []
248 lines = []
249 chunks.append(lines)
249 chunks.append(lines)
250
250
251 old_line, old_end, new_line, new_end = \
251 old_line, old_end, new_line, new_end = \
252 [int(x or 1) for x in match.groups()[:-1]]
252 [int(x or 1) for x in match.groups()[:-1]]
253 old_line -= 1
253 old_line -= 1
254 new_line -= 1
254 new_line -= 1
255 context = len(match.groups()) == 5
255 context = len(match.groups()) == 5
256 old_end += old_line
256 old_end += old_line
257 new_end += new_line
257 new_end += new_line
258
258
259 if context:
259 if context:
260 if not skipfirst:
260 if not skipfirst:
261 lines.append({
261 lines.append({
262 'old_lineno': '...',
262 'old_lineno': '...',
263 'new_lineno': '...',
263 'new_lineno': '...',
264 'action': 'context',
264 'action': 'context',
265 'line': line,
265 'line': line,
266 })
266 })
267 else:
267 else:
268 skipfirst = False
268 skipfirst = False
269
269
270 line = lineiter.next()
270 line = lineiter.next()
271 while old_line < old_end or new_line < new_end:
271 while old_line < old_end or new_line < new_end:
272 if line:
272 if line:
273 command, line = line[0], line[1:]
273 command, line = line[0], line[1:]
274 else:
274 else:
275 command = ' '
275 command = ' '
276 affects_old = affects_new = False
276 affects_old = affects_new = False
277
277
278 # ignore those if we don't expect them
278 # ignore those if we don't expect them
279 if command in '#@':
279 if command in '#@':
280 continue
280 continue
281 elif command == '+':
281 elif command == '+':
282 affects_new = True
282 affects_new = True
283 action = 'add'
283 action = 'add'
284 elif command == '-':
284 elif command == '-':
285 affects_old = True
285 affects_old = True
286 action = 'del'
286 action = 'del'
287 else:
287 else:
288 affects_old = affects_new = True
288 affects_old = affects_new = True
289 action = 'unmod'
289 action = 'unmod'
290
290
291 old_line += affects_old
291 old_line += affects_old
292 new_line += affects_new
292 new_line += affects_new
293 lines.append({
293 lines.append({
294 'old_lineno': affects_old and old_line or '',
294 'old_lineno': affects_old and old_line or '',
295 'new_lineno': affects_new and new_line or '',
295 'new_lineno': affects_new and new_line or '',
296 'action': action,
296 'action': action,
297 'line': line
297 'line': line
298 })
298 })
299 line = lineiter.next()
299 line = lineiter.next()
300
300
301 except StopIteration:
301 except StopIteration:
302 pass
302 pass
303
303
304 # highlight inline changes
304 # highlight inline changes
305 for file in files:
305 for file in files:
306 for chunk in chunks:
306 for chunk in chunks:
307 lineiter = iter(chunk)
307 lineiter = iter(chunk)
308 #first = True
308 #first = True
309 try:
309 try:
310 while 1:
310 while 1:
311 line = lineiter.next()
311 line = lineiter.next()
312 if line['action'] != 'unmod':
312 if line['action'] != 'unmod':
313 nextline = lineiter.next()
313 nextline = lineiter.next()
314 if nextline['action'] == 'unmod' or \
314 if nextline['action'] == 'unmod' or \
315 nextline['action'] == line['action']:
315 nextline['action'] == line['action']:
316 continue
316 continue
317 self.differ(line, nextline)
317 self.differ(line, nextline)
318 except StopIteration:
318 except StopIteration:
319 pass
319 pass
320
320
321 return files
321 return files
322
322
323 def prepare(self):
323 def prepare(self):
324 """
324 """
325 Prepare the passed udiff for HTML rendering. It'l return a list
325 Prepare the passed udiff for HTML rendering. It'l return a list
326 of dicts
326 of dicts
327 """
327 """
328 return self._parse_udiff()
328 return self._parse_udiff()
329
329
330 def _safe_id(self, idstring):
330 def _safe_id(self, idstring):
331 """Make a string safe for including in an id attribute.
331 """Make a string safe for including in an id attribute.
332
332
333 The HTML spec says that id attributes 'must begin with
333 The HTML spec says that id attributes 'must begin with
334 a letter ([A-Za-z]) and may be followed by any number
334 a letter ([A-Za-z]) and may be followed by any number
335 of letters, digits ([0-9]), hyphens ("-"), underscores
335 of letters, digits ([0-9]), hyphens ("-"), underscores
336 ("_"), colons (":"), and periods (".")'. These regexps
336 ("_"), colons (":"), and periods (".")'. These regexps
337 are slightly over-zealous, in that they remove colons
337 are slightly over-zealous, in that they remove colons
338 and periods unnecessarily.
338 and periods unnecessarily.
339
339
340 Whitespace is transformed into underscores, and then
340 Whitespace is transformed into underscores, and then
341 anything which is not a hyphen or a character that
341 anything which is not a hyphen or a character that
342 matches \w (alphanumerics and underscore) is removed.
342 matches \w (alphanumerics and underscore) is removed.
343
343
344 """
344 """
345 # Transform all whitespace to underscore
345 # Transform all whitespace to underscore
346 idstring = re.sub(r'\s', "_", '%s' % idstring)
346 idstring = re.sub(r'\s', "_", '%s' % idstring)
347 # Remove everything that is not a hyphen or a member of \w
347 # Remove everything that is not a hyphen or a member of \w
348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
349 return idstring
349 return idstring
350
350
351 def raw_diff(self):
351 def raw_diff(self):
352 """
352 """
353 Returns raw string as udiff
353 Returns raw string as udiff
354 """
354 """
355 udiff_copy = self.copy_iterator()
355 udiff_copy = self.copy_iterator()
356 if self.__format == 'gitdiff':
356 if self.__format == 'gitdiff':
357 udiff_copy = self._parse_gitdiff(udiff_copy)
357 udiff_copy = self._parse_gitdiff(udiff_copy)
358 return u''.join(udiff_copy)
358 return u''.join(udiff_copy)
359
359
360 def as_html(self, table_class='code-difftable', line_class='line',
360 def as_html(self, table_class='code-difftable', line_class='line',
361 new_lineno_class='lineno old', old_lineno_class='lineno new',
361 new_lineno_class='lineno old', old_lineno_class='lineno new',
362 code_class='code'):
362 code_class='code'):
363 """
363 """
364 Return udiff as html table with customized css classes
364 Return udiff as html table with customized css classes
365 """
365 """
366 def _link_to_if(condition, label, url):
366 def _link_to_if(condition, label, url):
367 """
367 """
368 Generates a link if condition is meet or just the label if not.
368 Generates a link if condition is meet or just the label if not.
369 """
369 """
370
370
371 if condition:
371 if condition:
372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
373 'label': label}
373 'label': label}
374 else:
374 else:
375 return label
375 return label
376 diff_lines = self.prepare()
376 diff_lines = self.prepare()
377 _html_empty = True
377 _html_empty = True
378 _html = []
378 _html = []
379 _html.append('''<table class="%(table_class)s">\n''' \
379 _html.append('''<table class="%(table_class)s">\n''' \
380 % {'table_class': table_class})
380 % {'table_class': table_class})
381 for diff in diff_lines:
381 for diff in diff_lines:
382 for line in diff['chunks']:
382 for line in diff['chunks']:
383 _html_empty = False
383 _html_empty = False
384 for change in line:
384 for change in line:
385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
386 % {'line_class': line_class,
386 % {'line_class': line_class,
387 'action': change['action']})
387 'action': change['action']})
388 anchor_old_id = ''
388 anchor_old_id = ''
389 anchor_new_id = ''
389 anchor_new_id = ''
390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
391 {'filename': self._safe_id(diff['filename']),
391 {'filename': self._safe_id(diff['filename']),
392 'oldline_no': change['old_lineno']}
392 'oldline_no': change['old_lineno']}
393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
394 {'filename': self._safe_id(diff['filename']),
394 {'filename': self._safe_id(diff['filename']),
395 'oldline_no': change['new_lineno']}
395 'oldline_no': change['new_lineno']}
396 cond_old = change['old_lineno'] != '...' and \
396 cond_old = change['old_lineno'] != '...' and \
397 change['old_lineno']
397 change['old_lineno']
398 cond_new = change['new_lineno'] != '...' and \
398 cond_new = change['new_lineno'] != '...' and \
399 change['new_lineno']
399 change['new_lineno']
400 if cond_old:
400 if cond_old:
401 anchor_old_id = 'id="%s"' % anchor_old
401 anchor_old_id = 'id="%s"' % anchor_old
402 if cond_new:
402 if cond_new:
403 anchor_new_id = 'id="%s"' % anchor_new
403 anchor_new_id = 'id="%s"' % anchor_new
404 ###########################################################
404 ###########################################################
405 # OLD LINE NUMBER
405 # OLD LINE NUMBER
406 ###########################################################
406 ###########################################################
407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
408 % {'a_id': anchor_old_id,
408 % {'a_id': anchor_old_id,
409 'old_lineno_cls': old_lineno_class})
409 'old_lineno_cls': old_lineno_class})
410
410
411 _html.append('''<pre>%(link)s</pre>''' \
411 _html.append('''<pre>%(link)s</pre>''' \
412 % {'link':
412 % {'link':
413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
414 % anchor_old)})
414 % anchor_old)})
415 _html.append('''</td>\n''')
415 _html.append('''</td>\n''')
416 ###########################################################
416 ###########################################################
417 # NEW LINE NUMBER
417 # NEW LINE NUMBER
418 ###########################################################
418 ###########################################################
419
419
420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
421 % {'a_id': anchor_new_id,
421 % {'a_id': anchor_new_id,
422 'new_lineno_cls': new_lineno_class})
422 'new_lineno_cls': new_lineno_class})
423
423
424 _html.append('''<pre>%(link)s</pre>''' \
424 _html.append('''<pre>%(link)s</pre>''' \
425 % {'link':
425 % {'link':
426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
427 % anchor_new)})
427 % anchor_new)})
428 _html.append('''</td>\n''')
428 _html.append('''</td>\n''')
429 ###########################################################
429 ###########################################################
430 # CODE
430 # CODE
431 ###########################################################
431 ###########################################################
432 _html.append('''\t<td class="%(code_class)s">''' \
432 _html.append('''\t<td class="%(code_class)s">''' \
433 % {'code_class': code_class})
433 % {'code_class': code_class})
434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
435 % {'code': change['line']})
435 % {'code': change['line']})
436 _html.append('''\t</td>''')
436 _html.append('''\t</td>''')
437 _html.append('''\n</tr>\n''')
437 _html.append('''\n</tr>\n''')
438 _html.append('''</table>''')
438 _html.append('''</table>''')
439 if _html_empty:
439 if _html_empty:
440 return None
440 return None
441 return ''.join(_html)
441 return ''.join(_html)
442
442
443 def stat(self):
443 def stat(self):
444 """
444 """
445 Returns tuple of added, and removed lines for this instance
445 Returns tuple of added, and removed lines for this instance
446 """
446 """
447 return self.adds, self.removes
447 return self.adds, self.removes
General Comments 0
You need to be logged in to leave comments. Login now