├── .gitignore ├── django_vcs ├── __init__.py ├── templatetags │ ├── __init__.py │ ├── udiff.py │ └── highlight.py ├── templates │ └── django_vcs │ │ ├── base.html │ │ ├── file_contents.html │ │ ├── folder_contents.html │ │ ├── repo_list.html │ │ ├── recent_commits.html │ │ ├── commit_detail.html │ │ ├── udiff.html │ │ └── diff_css.html ├── admin.py ├── urls.py ├── models.py ├── views.py └── diff.py ├── MANIFEST.in ├── README.txt ├── setup.py └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /django_vcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_vcs/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.txt 3 | recursive-include django_vcs/templates/django_vcs * 4 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | django-vcs 2 | ========== 3 | 4 | Requirements:: 5 | 6 | * pyvcs (Plus any backend specific dependencies, see the pyvcs README for more information) 7 | * pygments 8 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock %} 4 | {% block extra_head %}{% endblock %} 5 | 6 | 7 | {% block content %}{% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /django_vcs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django_vcs.models import CodeRepository 4 | 5 | class CodeRepositoryAdmin(admin.ModelAdmin): 6 | prepopulated_fields = { 7 | 'slug': ('name',) 8 | } 9 | 10 | admin.site.register(CodeRepository, CodeRepositoryAdmin) 11 | -------------------------------------------------------------------------------- /django_vcs/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | urlpatterns = patterns('django_vcs.views', 4 | url('^$', 'repo_list', name='repo_list'), 5 | url('^(?P[\w-]+)/$', 'recent_commits', name='recent_commits'), 6 | url('^(?P[\w-]+)/browser/(?P.*)$', 'code_browser', name='code_browser'), 7 | url('^(?P[\w-]+)/commit/(?P.*)/$', 'commit_detail', name='commit_detail'), 8 | ) 9 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/file_contents.html: -------------------------------------------------------------------------------- 1 | {% extends "django_vcs/base.html" %} 2 | 3 | {% load highlight %} 4 | 5 | {% block title %} 6 | {{ path }} in {{ repo.name }} 7 | {% endblock %} 8 | 9 | {% block extra_head %} 10 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 |

17 | Contents of {{ path }} 18 |

19 | {{ file|highlight:path }} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /django_vcs/templatetags/udiff.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.loader import render_to_string 3 | from django.utils.safestring import mark_safe 4 | 5 | from django_vcs.diff import prepare_udiff 6 | 7 | register = template.Library() 8 | 9 | @register.filter 10 | def render_diff(text): 11 | diffs, info = prepare_udiff(text) 12 | return render_to_string('django_vcs/udiff.html', {'diffs': diffs, 'info': info}) 13 | 14 | @register.inclusion_tag('django_vcs/diff_css.html') 15 | def diff_css(): 16 | return {} 17 | -------------------------------------------------------------------------------- /django_vcs/templatetags/highlight.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | 4 | from pygments import highlight 5 | from pygments.formatters import HtmlFormatter 6 | from pygments.lexers import guess_lexer_for_filename, TextLexer 7 | from pygments.util import ClassNotFound 8 | 9 | register = template.Library() 10 | 11 | @register.filter('highlight') 12 | def highlight_filter(text, filename): 13 | try: 14 | lexer = guess_lexer_for_filename(filename, text) 15 | except ClassNotFound: 16 | lexer = TextLexer() 17 | 18 | return mark_safe(highlight( 19 | text, 20 | lexer, 21 | HtmlFormatter(linenos="table", lineanchors="line") 22 | )) 23 | 24 | 25 | @register.simple_tag 26 | def highlight_css(): 27 | return HtmlFormatter(linenos="table", lineanchors="line").get_style_defs() 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = 'django-vcs', 5 | version = '0.1', 6 | author = 'Alex Gaynor, Justin Lilly', 7 | author_email = 'alex.gaynor@gmail.com', 8 | description = "A pluggable django application to allow browsing of code repositories.", 9 | url = 'http://github.com/alex/django-vcs/', 10 | packages = find_packages(), 11 | package_data = {'django_vcs': ['templates/django_vcs/*.html'],}, 12 | install_requires = ['pyvcs'], 13 | classifiers = [ 14 | 'Development Status :: 4 - Beta', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Programming Language :: Python', 18 | 'Topic :: Software Development :: Version Control', 19 | 'Environment :: Web Environment', 20 | 'Framework :: Django', 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/folder_contents.html: -------------------------------------------------------------------------------- 1 | {% extends "django_vcs/base.html" %} 2 | 3 | {% block title %} 4 | {{ path }} in {{ repo.name }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

9 | Contents of {{ path }} 10 |

11 |

Folders

12 | 21 |

Files

22 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/repo_list.html: -------------------------------------------------------------------------------- 1 | {% extends "django_vcs/base.html" %} 2 | 3 | {% block title %}Repository List{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for repo in repos %} 14 | 15 | 18 | 19 | {% with repo.get_recent_commits.0 as commit %} 20 | 21 | 22 | {% endwith %} 23 | 24 | {% endfor %} 25 |
NameVCSLast Commit DateLast Commit Message
16 | {{ repo.name }} 17 | {{ repo.get_repository_type_display }}{{ commit.time }}{{ commit.message }}
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/recent_commits.html: -------------------------------------------------------------------------------- 1 | {% extends "django_vcs/base.html" %} 2 | 3 | {% block title %} 4 | Recent commits for {{ repo.name }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

9 | Recent commits for {{ repo.name }} 10 |

11 | 12 | {% for commit in commits %} 13 | 14 | 19 | 22 | 25 | 28 | 29 | {% endfor %} 30 |
15 | 16 | {{ commit.commit_id }} 17 | 18 | 20 | {{ commit.author }} 21 | 23 | {{ commit.message }} 24 | 26 | {{ commit.item }} 27 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/commit_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "django_vcs/base.html" %} 2 | 3 | {% load udiff %} 4 | 5 | {% block title %} 6 | Details about commit {{ commit.commit_id }} on {{ repo.name }} 7 | {% endblock %} 8 | 9 | {% block extra_head %} 10 | {% diff_css %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

15 | Details about commit {{ commit.commit_id }} on {{ repo.name }} 16 |

17 | 33 | 34 | {{ commit.diff|render_diff }} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-vcs nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /django_vcs/models.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | from django.db import models 4 | 5 | from pyvcs.backends import AVAILABLE_BACKENDS, get_backend 6 | from pyvcs.exceptions import CommitDoesNotExist, FileDoesNotExist, FolderDoesNotExist 7 | 8 | 9 | REPOSITORY_TYPES = zip(count(), AVAILABLE_BACKENDS.keys()) 10 | 11 | class CodeRepository(models.Model): 12 | name = models.CharField(max_length=255) 13 | slug = models.SlugField() 14 | 15 | repository_type = models.IntegerField(choices=REPOSITORY_TYPES) 16 | 17 | location = models.CharField(max_length=255) 18 | 19 | class Meta: 20 | verbose_name_plural = "Code Repositories" 21 | 22 | def __unicode__(self): 23 | return "%s: %s" % (self.get_repository_type_display(), self.name) 24 | 25 | @models.permalink 26 | def get_absolute_url(self): 27 | return ('recent_commits', (), {'slug': self.slug}) 28 | 29 | @property 30 | def repo(self): 31 | if hasattr(self, '_repo'): 32 | return self._repo 33 | self._repo = get_backend(self.get_repository_type_display()).Repository(self.location) 34 | return self._repo 35 | 36 | def get_commit(self, commit_id): 37 | try: 38 | return self.repo.get_commit_by_id(str(commit_id)) 39 | except CommitDoesNotExist: 40 | return None 41 | 42 | def get_recent_commits(self, since=None): 43 | return self.repo.get_recent_commits(since=since) 44 | 45 | def get_folder_contents(self, path, rev=None): 46 | try: 47 | if rev is not None: 48 | rev = str(rev) 49 | return self.repo.list_directory(path, rev) 50 | except FolderDoesNotExist: 51 | return None 52 | 53 | def get_file_contents(self, path, rev=None): 54 | try: 55 | if rev is not None: 56 | rev = str(rev) 57 | return self.repo.file_contents(path, rev) 58 | except FileDoesNotExist: 59 | return None 60 | -------------------------------------------------------------------------------- /django_vcs/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.http import Http404 4 | from django.shortcuts import get_object_or_404, render_to_response 5 | from django.template import RequestContext 6 | 7 | from django_vcs.models import CodeRepository 8 | 9 | def repo_list(request): 10 | repos = CodeRepository.objects.all() 11 | return render_to_response('django_vcs/repo_list.html', {'repos': repos}, context_instance=RequestContext(request)) 12 | 13 | def recent_commits(request, slug): 14 | repo = get_object_or_404(CodeRepository, slug=slug) 15 | commits = repo.get_recent_commits() 16 | return render_to_response([ 17 | 'django_vcs/%s/recent_commits.html' % repo.name, 18 | 'django_vcs/recent_commits.html', 19 | ], {'repo': repo, 'commits': commits}, context_instance=RequestContext(request)) 20 | 21 | def code_browser(request, slug, path): 22 | repo = get_object_or_404(CodeRepository, slug=slug) 23 | rev = request.GET.get('rev') or None 24 | context = {'repo': repo, 'path': path} 25 | file_contents = repo.get_file_contents(path, rev) 26 | if file_contents is None: 27 | folder_contents = repo.get_folder_contents(path, rev) 28 | if folder_contents is None: 29 | raise Http404 30 | context['files'], context['folders'] = folder_contents 31 | context['files'] = [(os.path.join(path, o), o) for o in context['files']] 32 | context['folders'] = [(os.path.join(path, o), o) for o in context['folders']] 33 | return render_to_response([ 34 | 'django_vcs/%s/folder_contents.html' % repo.name, 35 | 'django_vcs/folder_contents.html', 36 | ], context, context_instance=RequestContext(request)) 37 | context['file'] = file_contents 38 | return render_to_response([ 39 | 'django_vcs/%s/file_contents.html' % repo.name, 40 | 'django_vcs/file_contents.html', 41 | ], context, context_instance=RequestContext(request)) 42 | 43 | def commit_detail(request, slug, commit_id): 44 | repo = get_object_or_404(CodeRepository, slug=slug) 45 | commit = repo.get_commit(commit_id) 46 | if commit is None: 47 | raise Http404 48 | return render_to_response([ 49 | 'django_vcs/%s/commit_detail.html' % repo.name, 50 | 'django_vcs/commit_detail.html', 51 | ], {'repo': repo, 'commit': commit}, context_instance=RequestContext(request)) 52 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/udiff.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if info %} 4 |
5 | {% for k, v in info %} 6 | {% if k %} 7 | {{ k }}: 8 | {% endif %} 9 | {{ v|linebreaksbr }} 10 | {% if not forloop.last %}
{% endif %} 11 | {% endfor %} 12 |
13 | {% endif %} 14 | {% for diff in diffs %} 15 |
16 | {% if diff.is_header %} 17 |
18 |                         {{ diff.lines|join:"\n" }}
19 |                     
20 | {% else %} 21 |
22 |
23 | 24 | {{ diff.old_filename }} 25 | 26 | {% if diff.old_revision %} 27 | 28 | [{{ diff.old_revision }}] 29 | 30 | {% endif %} 31 |
32 |
33 | 34 | {{ diff.new_filename }} 35 | 36 | {% if diff.new_revision %} 37 | 38 | [{{ diff.new_revision }}] 39 | 40 | {% endif %} 41 |
42 |
43 | 44 | {% for chunk in diff.chunks %} 45 | {% if not forloop.first %} 46 | 47 | 48 | 49 | {% endif %} 50 | {% for line in chunk %} 51 | 52 | 53 | 54 | 55 | 56 | {% endfor %} 57 | {% endfor %} 58 |
{{ line.old_lineno }}{{ line.new_lineno }}{{ line.line|safe }}
59 | {% endif %} 60 |
61 | {% endfor %} 62 |
63 |
64 | -------------------------------------------------------------------------------- /django_vcs/templates/django_vcs/diff_css.html: -------------------------------------------------------------------------------- 1 | 147 | -------------------------------------------------------------------------------- /django_vcs/diff.py: -------------------------------------------------------------------------------- 1 | """ 2 | Most of this code is taken right out of lodgit: 3 | http://dev.pocoo.org/projects/lodgeit/ 4 | """ 5 | 6 | import re 7 | 8 | from django.utils.html import escape 9 | 10 | def prepare_udiff(udiff): 11 | return DiffRenderer(udiff).prepare() 12 | 13 | class DiffRenderer(object): 14 | _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@') 15 | 16 | def __init__(self, udiff): 17 | self.lines = [escape(line) for line in udiff.splitlines()] 18 | 19 | def prepare(self): 20 | return self._parse_udiff() 21 | 22 | def _parse_udiff(self): 23 | info = self._parse_info() 24 | 25 | in_header = True 26 | header = [] 27 | lineiter = iter(self.lines) 28 | files = [] 29 | try: 30 | line = lineiter.next() 31 | while True: 32 | if not line.startswith('--- '): 33 | if in_header: 34 | header.append(line) 35 | line = lineiter.next() 36 | continue 37 | 38 | if header and all(o.strip() for o in header): 39 | files.append({'is_header': True, 'lines': header}) 40 | header = [] 41 | 42 | in_header = [] 43 | chunks = [] 44 | old, new = self._extract_rev(line, lineiter.next()) 45 | files.append({ 46 | 'is_header': False, 47 | 'old_filename': old[0], 48 | 'old_revision': old[1], 49 | 'new_filename': new[0], 50 | 'new_revision': new[1], 51 | 'chunks': chunks, 52 | }) 53 | 54 | line = lineiter.next() 55 | while line: 56 | match = self._chunk_re.match(line) 57 | if not match: 58 | in_header = False 59 | break 60 | 61 | lines = [] 62 | chunks.append(lines) 63 | 64 | old_line, old_end, new_line, new_end = [int(o or 1) for o in match.groups()] 65 | old_line -= 1 66 | new_line -= 1 67 | old_end += old_line 68 | new_end += new_line 69 | line = lineiter.next() 70 | 71 | while old_line < old_end or new_line < new_end: 72 | if line: 73 | command, line = line[0], line[1:] 74 | else: 75 | command = ' ' 76 | affects_old = affects_new = False 77 | 78 | if command == '+': 79 | affects_new = True 80 | action = 'add' 81 | elif command == '-': 82 | affects_old = True 83 | action = 'del' 84 | else: 85 | affects_old = affects_new = True 86 | action = 'unmod' 87 | 88 | old_line += affects_old 89 | new_line += affects_new 90 | lines.append({ 91 | 'old_lineno': affects_old and old_line or u'', 92 | 'new_lineno': affects_new and new_line or u'', 93 | 'action': action, 94 | 'line': line, 95 | }) 96 | line = lineiter.next() 97 | except StopIteration: 98 | pass 99 | 100 | for file in files: 101 | if file['is_header']: 102 | continue 103 | for chunk in file['chunks']: 104 | lineiter = iter(chunk) 105 | first = True 106 | try: 107 | while True: 108 | line = lineiter.next() 109 | if line['action'] != 'unmod': 110 | nextline = lineiter.next() 111 | if nextline['action'] == 'unmod' or nextline['action'] == line['action']: 112 | continue 113 | self._highlight_line(line, nextline) 114 | except StopIteration: 115 | pass 116 | 117 | return files, info 118 | 119 | def _parse_info(self): 120 | nlines = len(self.lines) 121 | if not nlines: 122 | return 123 | firstline = self.lines[0] 124 | info = [] 125 | 126 | # todo copy the HG stuff 127 | 128 | return info 129 | 130 | def _extract_rev(self, line1, line2): 131 | def _extract(line): 132 | parts = line.split(None, 1) 133 | return parts[0], (len(parts) == 2 and parts[1] or None) 134 | 135 | try: 136 | if line1.startswith('--- ') and line2.startswith('+++ '): 137 | return _extract(line1[4:]), _extract(line2[4:]) 138 | except (ValueError, IndexError): 139 | pass 140 | return (None, None), (None, None) 141 | 142 | def _highlight_line(self, line, next): 143 | start = 0 144 | limit = min(len(line['line']), len(next['line'])) 145 | while start < limit and line['line'][start] == next['line'][start]: 146 | start += 1 147 | end = -1 148 | limit -= start 149 | while -end <= limit and line['line'][end] == next['line'][end]: 150 | end -= 1 151 | end += 1 152 | if start or end: 153 | def do(l): 154 | last = end + len(l['line']) 155 | if l['action'] == 'add': 156 | tag = 'ins' 157 | else: 158 | tag = 'del' 159 | l['line'] = u'%s<%s>%s%s' % ( 160 | l['line'][:start], 161 | tag, 162 | l['line'][start:last], 163 | tag, 164 | l['line'][last:], 165 | ) 166 | do(line) 167 | do(next) 168 | --------------------------------------------------------------------------------