##// END OF EJS Templates
Let the function calculating changeset history do all the work...
marcink -
r2981:6cd0f8f8 beta
parent child Browse files
Show More
@@ -1,558 +1,556
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) 2010-2012 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 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib import diffs
37 37 from rhodecode.lib import helpers as h
38 38
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
41 41 str2bool
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 45 from rhodecode.lib.vcs.conf import settings
46 46 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 47 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 48 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
49 NodeDoesNotExistError
49 NodeDoesNotExistError, ChangesetError
50 50 from rhodecode.lib.vcs.nodes import FileNode
51 51
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.db import Repository
55 55
56 56 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 57 _context_url, get_line_ctx, get_ignore_ws
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class FilesController(BaseRepoController):
64 64
65 65 def __before__(self):
66 66 super(FilesController, self).__before__()
67 67 c.cut_off_limit = self.cut_off_limit
68 68
69 69 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 70 """
71 71 Safe way to get changeset if error occur it redirects to tip with
72 72 proper message
73 73
74 74 :param rev: revision to fetch
75 75 :param repo_name: repo name to redirect after
76 76 """
77 77
78 78 try:
79 79 return c.rhodecode_repo.get_changeset(rev)
80 80 except EmptyRepositoryError, e:
81 81 if not redirect_after:
82 82 return None
83 83 url_ = url('files_add_home',
84 84 repo_name=c.repo_name,
85 85 revision=0, f_path='')
86 86 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
87 87 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 88 category='warning')
89 89 redirect(h.url('summary_home', repo_name=repo_name))
90 90
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 94
95 95 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 96 """
97 97 Returns file_node, if error occurs or given path is directory,
98 98 it'll redirect to top level path
99 99
100 100 :param repo_name: repo_name
101 101 :param cs: given changeset
102 102 :param path: path to lookup
103 103 """
104 104
105 105 try:
106 106 file_node = cs.get_node(path)
107 107 if file_node.is_dir():
108 108 raise RepositoryError('given path is a directory')
109 109 except RepositoryError, e:
110 110 h.flash(str(e), category='warning')
111 111 redirect(h.url('files_home', repo_name=repo_name,
112 112 revision=cs.raw_id))
113 113
114 114 return file_node
115 115
116 116 @LoginRequired()
117 117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 118 'repository.admin')
119 119 def index(self, repo_name, revision, f_path, annotate=False):
120 120 # redirect to given revision from form if given
121 121 post_revision = request.POST.get('at_rev', None)
122 122 if post_revision:
123 123 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 124 redirect(url('files_home', repo_name=c.repo_name,
125 125 revision=cs.raw_id, f_path=f_path))
126 126
127 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 128 c.branch = request.GET.get('branch', None)
129 129 c.f_path = f_path
130 130 c.annotate = annotate
131 131 cur_rev = c.changeset.revision
132 132
133 133 # prev link
134 134 try:
135 135 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 136 c.url_prev = url('files_home', repo_name=c.repo_name,
137 137 revision=prev_rev.raw_id, f_path=f_path)
138 138 if c.branch:
139 139 c.url_prev += '?branch=%s' % c.branch
140 140 except (ChangesetDoesNotExistError, VCSError):
141 141 c.url_prev = '#'
142 142
143 143 # next link
144 144 try:
145 145 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 146 c.url_next = url('files_home', repo_name=c.repo_name,
147 147 revision=next_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_next += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_next = '#'
152 152
153 153 # files or dirs
154 154 try:
155 155 c.file = c.changeset.get_node(f_path)
156 156
157 157 if c.file.is_file():
158 _hist = c.rhodecode_repo.get_changeset().get_file_history(f_path)
158 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
159 159 c.file_changeset = c.changeset
160 160 if _hist:
161 161 c.file_changeset = (c.changeset
162 162 if c.changeset.revision < _hist[0].revision
163 163 else _hist[0])
164 c.file_history = self._get_node_history(c.changeset, f_path,
165 _hist)
166 164 c.authors = []
167 165 for a in set([x.author for x in _hist]):
168 166 c.authors.append((h.email(a), h.person(a)))
169 167 else:
170 168 c.authors = c.file_history = []
171 169 except RepositoryError, e:
172 170 h.flash(str(e), category='warning')
173 171 redirect(h.url('files_home', repo_name=repo_name,
174 172 revision='tip'))
175 173
176 174 if request.environ.get('HTTP_X_PARTIAL_XHR'):
177 175 return render('files/files_ypjax.html')
178 176
179 177 return render('files/files.html')
180 178
181 179 @LoginRequired()
182 180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
183 181 'repository.admin')
184 182 def rawfile(self, repo_name, revision, f_path):
185 183 cs = self.__get_cs_or_redirect(revision, repo_name)
186 184 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
187 185
188 186 response.content_disposition = 'attachment; filename=%s' % \
189 187 safe_str(f_path.split(Repository.url_sep())[-1])
190 188
191 189 response.content_type = file_node.mimetype
192 190 return file_node.content
193 191
194 192 @LoginRequired()
195 193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
196 194 'repository.admin')
197 195 def raw(self, repo_name, revision, f_path):
198 196 cs = self.__get_cs_or_redirect(revision, repo_name)
199 197 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
200 198
201 199 raw_mimetype_mapping = {
202 200 # map original mimetype to a mimetype used for "show as raw"
203 201 # you can also provide a content-disposition to override the
204 202 # default "attachment" disposition.
205 203 # orig_type: (new_type, new_dispo)
206 204
207 205 # show images inline:
208 206 'image/x-icon': ('image/x-icon', 'inline'),
209 207 'image/png': ('image/png', 'inline'),
210 208 'image/gif': ('image/gif', 'inline'),
211 209 'image/jpeg': ('image/jpeg', 'inline'),
212 210 'image/svg+xml': ('image/svg+xml', 'inline'),
213 211 }
214 212
215 213 mimetype = file_node.mimetype
216 214 try:
217 215 mimetype, dispo = raw_mimetype_mapping[mimetype]
218 216 except KeyError:
219 217 # we don't know anything special about this, handle it safely
220 218 if file_node.is_binary:
221 219 # do same as download raw for binary files
222 220 mimetype, dispo = 'application/octet-stream', 'attachment'
223 221 else:
224 222 # do not just use the original mimetype, but force text/plain,
225 223 # otherwise it would serve text/html and that might be unsafe.
226 224 # Note: underlying vcs library fakes text/plain mimetype if the
227 225 # mimetype can not be determined and it thinks it is not
228 226 # binary.This might lead to erroneous text display in some
229 227 # cases, but helps in other cases, like with text files
230 228 # without extension.
231 229 mimetype, dispo = 'text/plain', 'inline'
232 230
233 231 if dispo == 'attachment':
234 232 dispo = 'attachment; filename=%s' % \
235 233 safe_str(f_path.split(os.sep)[-1])
236 234
237 235 response.content_disposition = dispo
238 236 response.content_type = mimetype
239 237 return file_node.content
240 238
241 239 @LoginRequired()
242 240 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
243 241 def edit(self, repo_name, revision, f_path):
244 242 repo = Repository.get_by_repo_name(repo_name)
245 243 if repo.enable_locking and repo.locked[0]:
246 244 h.flash(_('This repository is has been locked by %s on %s')
247 245 % (h.person_by_id(repo.locked[0]),
248 246 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
249 247 'warning')
250 248 return redirect(h.url('files_home',
251 249 repo_name=repo_name, revision='tip'))
252 250
253 251 r_post = request.POST
254 252
255 253 c.cs = self.__get_cs_or_redirect(revision, repo_name)
256 254 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
257 255
258 256 if c.file.is_binary:
259 257 return redirect(url('files_home', repo_name=c.repo_name,
260 258 revision=c.cs.raw_id, f_path=f_path))
261 259
262 260 c.f_path = f_path
263 261
264 262 if r_post:
265 263
266 264 old_content = c.file.content
267 265 sl = old_content.splitlines(1)
268 266 first_line = sl[0] if sl else ''
269 267 # modes: 0 - Unix, 1 - Mac, 2 - DOS
270 268 mode = detect_mode(first_line, 0)
271 269 content = convert_line_endings(r_post.get('content'), mode)
272 270
273 271 message = r_post.get('message') or (_('Edited %s via RhodeCode')
274 272 % (f_path))
275 273 author = self.rhodecode_user.full_contact
276 274
277 275 if content == old_content:
278 276 h.flash(_('No changes'),
279 277 category='warning')
280 278 return redirect(url('changeset_home', repo_name=c.repo_name,
281 279 revision='tip'))
282 280
283 281 try:
284 282 self.scm_model.commit_change(repo=c.rhodecode_repo,
285 283 repo_name=repo_name, cs=c.cs,
286 284 user=self.rhodecode_user,
287 285 author=author, message=message,
288 286 content=content, f_path=f_path)
289 287 h.flash(_('Successfully committed to %s') % f_path,
290 288 category='success')
291 289
292 290 except Exception:
293 291 log.error(traceback.format_exc())
294 292 h.flash(_('Error occurred during commit'), category='error')
295 293 return redirect(url('changeset_home',
296 294 repo_name=c.repo_name, revision='tip'))
297 295
298 296 return render('files/files_edit.html')
299 297
300 298 @LoginRequired()
301 299 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
302 300 def add(self, repo_name, revision, f_path):
303 301
304 302 repo = Repository.get_by_repo_name(repo_name)
305 303 if repo.enable_locking and repo.locked[0]:
306 304 h.flash(_('This repository is has been locked by %s on %s')
307 305 % (h.person_by_id(repo.locked[0]),
308 306 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
309 307 'warning')
310 308 return redirect(h.url('files_home',
311 309 repo_name=repo_name, revision='tip'))
312 310
313 311 r_post = request.POST
314 312 c.cs = self.__get_cs_or_redirect(revision, repo_name,
315 313 redirect_after=False)
316 314 if c.cs is None:
317 315 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
318 316
319 317 c.f_path = f_path
320 318
321 319 if r_post:
322 320 unix_mode = 0
323 321 content = convert_line_endings(r_post.get('content'), unix_mode)
324 322
325 323 message = r_post.get('message') or (_('Added %s via RhodeCode')
326 324 % (f_path))
327 325 location = r_post.get('location')
328 326 filename = r_post.get('filename')
329 327 file_obj = r_post.get('upload_file', None)
330 328
331 329 if file_obj is not None and hasattr(file_obj, 'filename'):
332 330 filename = file_obj.filename
333 331 content = file_obj.file
334 332
335 333 node_path = os.path.join(location, filename)
336 334 author = self.rhodecode_user.full_contact
337 335
338 336 if not content:
339 337 h.flash(_('No content'), category='warning')
340 338 return redirect(url('changeset_home', repo_name=c.repo_name,
341 339 revision='tip'))
342 340 if not filename:
343 341 h.flash(_('No filename'), category='warning')
344 342 return redirect(url('changeset_home', repo_name=c.repo_name,
345 343 revision='tip'))
346 344
347 345 try:
348 346 self.scm_model.create_node(repo=c.rhodecode_repo,
349 347 repo_name=repo_name, cs=c.cs,
350 348 user=self.rhodecode_user,
351 349 author=author, message=message,
352 350 content=content, f_path=node_path)
353 351 h.flash(_('Successfully committed to %s') % node_path,
354 352 category='success')
355 353 except NodeAlreadyExistsError, e:
356 354 h.flash(_(e), category='error')
357 355 except Exception:
358 356 log.error(traceback.format_exc())
359 357 h.flash(_('Error occurred during commit'), category='error')
360 358 return redirect(url('changeset_home',
361 359 repo_name=c.repo_name, revision='tip'))
362 360
363 361 return render('files/files_add.html')
364 362
365 363 @LoginRequired()
366 364 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 365 'repository.admin')
368 366 def archivefile(self, repo_name, fname):
369 367
370 368 fileformat = None
371 369 revision = None
372 370 ext = None
373 371 subrepos = request.GET.get('subrepos') == 'true'
374 372
375 373 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
376 374 archive_spec = fname.split(ext_data[1])
377 375 if len(archive_spec) == 2 and archive_spec[1] == '':
378 376 fileformat = a_type or ext_data[1]
379 377 revision = archive_spec[0]
380 378 ext = ext_data[1]
381 379
382 380 try:
383 381 dbrepo = RepoModel().get_by_repo_name(repo_name)
384 382 if dbrepo.enable_downloads is False:
385 383 return _('downloads disabled')
386 384
387 385 if c.rhodecode_repo.alias == 'hg':
388 386 # patch and reset hooks section of UI config to not run any
389 387 # hooks on fetching archives with subrepos
390 388 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
391 389 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
392 390
393 391 cs = c.rhodecode_repo.get_changeset(revision)
394 392 content_type = settings.ARCHIVE_SPECS[fileformat][0]
395 393 except ChangesetDoesNotExistError:
396 394 return _('Unknown revision %s') % revision
397 395 except EmptyRepositoryError:
398 396 return _('Empty repository')
399 397 except (ImproperArchiveTypeError, KeyError):
400 398 return _('Unknown archive type')
401 399
402 400 fd, archive = tempfile.mkstemp()
403 401 t = open(archive, 'wb')
404 402 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
405 403 t.close()
406 404
407 405 def get_chunked_archive(archive):
408 406 stream = open(archive, 'rb')
409 407 while True:
410 408 data = stream.read(16 * 1024)
411 409 if not data:
412 410 stream.close()
413 411 os.close(fd)
414 412 os.remove(archive)
415 413 break
416 414 yield data
417 415
418 416 response.content_disposition = str('attachment; filename=%s-%s%s' \
419 417 % (repo_name, revision[:12], ext))
420 418 response.content_type = str(content_type)
421 419 return get_chunked_archive(archive)
422 420
423 421 @LoginRequired()
424 422 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
425 423 'repository.admin')
426 424 def diff(self, repo_name, f_path):
427 425 ignore_whitespace = request.GET.get('ignorews') == '1'
428 426 line_context = request.GET.get('context', 3)
429 427 diff1 = request.GET.get('diff1', '')
430 428 diff2 = request.GET.get('diff2', '')
431 429 c.action = request.GET.get('diff')
432 430 c.no_changes = diff1 == diff2
433 431 c.f_path = f_path
434 432 c.big_diff = False
435 433 c.anchor_url = anchor_url
436 434 c.ignorews_url = _ignorews_url
437 435 c.context_url = _context_url
438 436 c.changes = OrderedDict()
439 437 c.changes[diff2] = []
440 438
441 439 #special case if we want a show rev only, it's impl here
442 440 #to reduce JS and callbacks
443 441 if request.GET.get('show_rev'):
444 442 if str2bool(request.GET.get('annotate', 'False')):
445 443 _url = url('files_annotate_home', repo_name=c.repo_name,
446 444 revision=diff1, f_path=c.f_path)
447 445 else:
448 446 _url = url('files_home', repo_name=c.repo_name,
449 447 revision=diff1, f_path=c.f_path)
450 448
451 449 return redirect(_url)
452 450 try:
453 451 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
454 452 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
455 453 node1 = c.changeset_1.get_node(f_path)
456 454 else:
457 455 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
458 456 node1 = FileNode('.', '', changeset=c.changeset_1)
459 457
460 458 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
461 459 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
462 460 node2 = c.changeset_2.get_node(f_path)
463 461 else:
464 462 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
465 463 node2 = FileNode('.', '', changeset=c.changeset_2)
466 464 except RepositoryError:
467 465 return redirect(url('files_home', repo_name=c.repo_name,
468 466 f_path=f_path))
469 467
470 468 if c.action == 'download':
471 469 _diff = diffs.get_gitdiff(node1, node2,
472 470 ignore_whitespace=ignore_whitespace,
473 471 context=line_context)
474 472 diff = diffs.DiffProcessor(_diff, format='gitdiff')
475 473
476 474 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
477 475 response.content_type = 'text/plain'
478 476 response.content_disposition = (
479 477 'attachment; filename=%s' % diff_name
480 478 )
481 479 return diff.raw_diff()
482 480
483 481 elif c.action == 'raw':
484 482 _diff = diffs.get_gitdiff(node1, node2,
485 483 ignore_whitespace=ignore_whitespace,
486 484 context=line_context)
487 485 diff = diffs.DiffProcessor(_diff, format='gitdiff')
488 486 response.content_type = 'text/plain'
489 487 return diff.raw_diff()
490 488
491 489 else:
492 490 fid = h.FID(diff2, node2.path)
493 491 line_context_lcl = get_line_ctx(fid, request.GET)
494 492 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
495 493
496 494 lim = request.GET.get('fulldiff') or self.cut_off_limit
497 495 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
498 496 filenode_new=node2,
499 497 cut_off_limit=lim,
500 498 ignore_whitespace=ign_whitespace_lcl,
501 499 line_context=line_context_lcl,
502 500 enable_comments=False)
503 501
504 502 c.changes = [('', node2, diff, cs1, cs2, st,)]
505 503
506 504 return render('files/file_diff.html')
507 505
508 506 def _get_node_history(self, cs, f_path, changesets=None):
509 507 """
510 508 get changesets history for given node
511 509
512 510 :param cs: changeset to calculate history
513 511 :param f_path: path for node to calculate history for
514 512 :param changesets: if passed don't calculate history and take
515 513 changesets defined in this list
516 514 """
517 515 # calculate history based on tip
518 516 tip_cs = c.rhodecode_repo.get_changeset()
519 517 if changesets is None:
520 518 try:
521 519 changesets = tip_cs.get_file_history(f_path)
522 except NodeDoesNotExistError:
520 except (NodeDoesNotExistError, ChangesetError):
523 521 #this node is not present at tip !
524 522 changesets = cs.get_file_history(f_path)
525 523
526 524 hist_l = []
527 525
528 526 changesets_group = ([], _("Changesets"))
529 527 branches_group = ([], _("Branches"))
530 528 tags_group = ([], _("Tags"))
531 529 _hg = cs.repository.alias == 'hg'
532 530 for chs in changesets:
533 531 _branch = '(%s)' % chs.branch if _hg else ''
534 532 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
535 533 changesets_group[0].append((chs.raw_id, n_desc,))
536 534
537 535 hist_l.append(changesets_group)
538 536
539 537 for name, chs in c.rhodecode_repo.branches.items():
540 538 branches_group[0].append((chs, name),)
541 539 hist_l.append(branches_group)
542 540
543 541 for name, chs in c.rhodecode_repo.tags.items():
544 542 tags_group[0].append((chs, name),)
545 543 hist_l.append(tags_group)
546 544
547 return hist_l
545 return hist_l, changesets
548 546
549 547 @LoginRequired()
550 548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
551 549 'repository.admin')
552 550 @jsonify
553 551 def nodelist(self, repo_name, revision, f_path):
554 552 if request.environ.get('HTTP_X_PARTIAL_XHR'):
555 553 cs = self.__get_cs_or_redirect(revision, repo_name)
556 554 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
557 555 flat=False)
558 556 return {'nodes': _d + _f}
General Comments 0
You need to be logged in to leave comments. Login now