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