├── .gitignore ├── .gitmodules ├── COPYING ├── MANIFEST.in ├── README ├── configs ├── goblet.conf ├── goblet.nginx.conf └── uwsgi.ini ├── docs ├── Makefile ├── conf.py ├── configuring.rst ├── index.rst └── install.rst ├── goblet ├── __init__.py ├── __main__.py ├── encoding.py ├── filters.py ├── json_views.py ├── memoize.py ├── monkey.py ├── render.py ├── themes │ └── default │ │ ├── static │ │ ├── body.jpg │ │ ├── chosen │ │ ├── chosen.css │ │ ├── favicon.ico │ │ ├── file_icon.png │ │ ├── folder_icon.png │ │ ├── goblet.css │ │ ├── goblet.js │ │ ├── link_icon.png │ │ ├── logo.png │ │ ├── pygments.css │ │ ├── repo_icon.png │ │ ├── reset.css │ │ ├── script_icon.png │ │ ├── search.png │ │ └── up_icon.png │ │ └── templates │ │ ├── base.html │ │ ├── blob.html │ │ ├── commit.html │ │ ├── log.html │ │ ├── nocommits.html │ │ ├── repo_base.html │ │ ├── repo_index.html │ │ ├── search.html │ │ ├── tags.html │ │ └── tree.html └── views.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python & vim byproducts 2 | *.py? 3 | __pycache__ 4 | .*.sw? 5 | 6 | # uwsgi byproducts 7 | *.pid 8 | *.sock 9 | *.log 10 | 11 | # Build byproducts 12 | build 13 | docs/_build 14 | dist 15 | MANIFEST 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "chosen"] 2 | path = chosen 3 | url = git://github.com/harvesthq/chosen.git 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Goblet - Web based git repository browser 2 | Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | Shipped with goblet is the 'chosen' javascript library, which has the following 15 | license: 16 | 17 | # Chosen, a Select Box Enhancer for jQuery and Protoype 18 | ## by Patrick Filler for [Harvest](http://getharvest.com) 19 | 20 | Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License) 21 | 22 | Copyright (c) 2011 by Harvest 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in 32 | all copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 40 | THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include docs/*.rst 2 | include docs/_static/* 3 | include docs/Makefile 4 | include configs/*.conf 5 | include configs/*.ini 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Goblet - git repository viewer 2 | ============================== 3 | Goblet is a fast, easy to customize web frontend for git repositories. It was 4 | created because existing alternatives are either not very easy to customize 5 | (gitweb), require C programming to do so (cgit), or are tied into other 6 | products, such as bugtrackers (redmin, github). 7 | 8 | Goblet is currently in alpha status, so not all goals have been met yet. 9 | Contributions are welcome, the most useful contribution is using it and 10 | reporting all issues you have. 11 | 12 | If you want to see goblet in action, you can find a running instance on 13 | git.kaarsemaker.net. If you like what you see, proceed to read docs/install.rst 14 | and enjoy! 15 | 16 | Should you hit a problem installing or using goblet, please report it on 17 | github. Reports about uncaught python exceptions should include full 18 | backtraces. If the repository triggering the bug/issue is public, please 19 | include a link to the repository and the link to the bug so I can reproduce it. 20 | -------------------------------------------------------------------------------- /configs/goblet.conf: -------------------------------------------------------------------------------- 1 | # Example goblet configuration that mimics the built-in defaults. Tweak as you 2 | # see fit, especially paths on the filesystem and the ABOUT setting. 3 | 4 | import os 5 | dirname = os.path.dirname 6 | 7 | # We search one root directory for repositories, we may search more than one 8 | # level deep to find repositories in subdirs. By default we search the 9 | # directory containing the checkout of goblet as that will contain at least one 10 | # git repository. 11 | REPO_ROOT = dirname(dirname(dirname(__file__))) 12 | MAX_SEARCH_DEPTH = 3 13 | 14 | # Snapshots are cached here. They are not cleaned up automatically, you can use 15 | # tmpwatch/tmpreaper for this if you want to. 16 | CACHE_ROOT = '/tmp/goblet-snapshots' 17 | 18 | # Goblet can tell the user where to clone from, but you'll need to tell goblet 19 | # where. The repository name is appended to the base urls you specify here. 20 | CLONE_URLS_BASE = { 21 | 'git': 'git://git.example.com', 22 | 'http': 'https://git.example.com', 23 | # Don't show ssh urls by default 24 | # 'ssh': 'example.com:/srv/gitroot', 25 | } 26 | 27 | # Use the default theme. This can be set to either a theme name (folder in 28 | # goblet/themes) or a full path to your custom theme somewhere on the 29 | # filesystem. The theming system has no fallback, so you will need to implement 30 | # all templates. Any static files you want from an existing theme need to be 31 | # copied too. 32 | THEME = "default" 33 | 34 | # On the main index page, the right column is reserved for an 'about this site' 35 | # blurb, which by default contains some uninspiring text about git & goblet. 36 | # Replace it with something better please. 37 | ABOUT = """
39 | Git is a free and open source distributed 40 | version control system designed to handle everything from small to very large 41 | projects with speed and efficiency. 42 |
43 |44 | Goblet is a fast web-based git repository browser using libgit2. 45 |
""" 46 | 47 | # When not in debug mode, error 500 reports are sent to ADMINS from SENDER 48 | ADMINS = ['your_mailaddress@example.com'] 49 | SENDER = 'webmaster@localhost' 50 | 51 | # Flask config, see 52 | # http://flask.pocoo.org/docs/config/#builtin-configuration-values 53 | 54 | # Use debug mode based on the GOBLET_DEBUG environment variable 55 | DEBUG = os.environ.get('GOBLET_DEBUG', 'False').lower() == 'true' 56 | 57 | # Use X-Sendfile headers so snapshots will be sent by the webserver, not goblet. 58 | USE_X_SENDFILE = True 59 | # This is needed to make X-Sendfile work under nginx as well 60 | USE_X_ACCEL_REDIRECT = True 61 | 62 | # vim:syntax=python 63 | -------------------------------------------------------------------------------- /configs/goblet.nginx.conf: -------------------------------------------------------------------------------- 1 | # Example nginx virtual host config for goblet. You will need to (at least) 2 | # modify $repo_root and $goblet_root to make it work for you. Also note that 3 | # goblet can at the moment only run in the root path of a host. 4 | 5 | server { 6 | # Path to the directory all the repositories are in 7 | set $repo_root /home/dennis/code; 8 | # Path to the goblet code 9 | set $goblet_root /home/dennis/code/goblet; 10 | 11 | listen 80; 12 | 13 | # Set root to the repository directory so we can serve the repositories and 14 | # goblet from the same host. If you do not want this, set root to an empty 15 | # dir, disable git-http-backend below, disable the try_files line and 16 | # change location @uwsgi to location / 17 | root $repo_root; 18 | server_name localhost; 19 | 20 | # Use git's smart HTTP backend for quicker cloning 21 | location ~ ^.*/(HEAD|info/refs|objects/info/.*|git-(upload|receive)-pack)$ { 22 | fastcgi_pass unix:/var/run/fcgiwrap.socket; 23 | fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; 24 | fastcgi_param PATH_INFO $uri; 25 | fastcgi_param GIT_PROJECT_ROOT $repo_root; 26 | fastcgi_param REMOTE_USER $remote_user; 27 | include fastcgi_params; 28 | } 29 | 30 | # Try to find files first. If no file exists, forward to goblet's uwsgi 31 | # socket. 32 | try_files $uri @uwsgi; 33 | location @uwsgi { 34 | include uwsgi_params; 35 | uwsgi_pass unix:/tmp/uwsgi.sock; 36 | } 37 | 38 | # Static content is served from the goblet checkout. 39 | location /static/ { 40 | alias $goblet_root/goblet/themes/default/static/; 41 | } 42 | 43 | # Snapshots are served using the X-SendFile mechanism, or in nginx terms: 44 | # X-Accel-Redirect. Make sure the directory specified here matches 45 | # CACHE_ROOT in your goblet.conf, or you'll see no snapshots whatsoever. 46 | location /snapshots/ { 47 | internal; 48 | alias /tmp/goblet-snapshots/; 49 | } 50 | } 51 | 52 | # Perl you say? Well, that makes it surprisingly readable! 53 | # vim:syntax=perl 54 | -------------------------------------------------------------------------------- /configs/uwsgi.ini: -------------------------------------------------------------------------------- 1 | # Example uwsgi config for goblet. Tweak as you see fit, especially the 2 | # location of socket, log and pid file are suitable for demonstration only, not 3 | # for production environments. The same holds for the environment vaiables. 4 | 5 | [uwsgi] 6 | 7 | # Load goblet from the git checkout this file is in by default 8 | plugins = python 9 | module = goblet:app 10 | env = GOBLET_SETTINGS=%d/goblet.conf 11 | env = PYTHONPATH=%d/.. 12 | env = HOME=/nonexistent 13 | 14 | # Basic management setup 15 | shared-socket = 1 16 | pidfile = /run/uwsgi.pid 17 | socket = /tmp/uwsgi.sock 18 | chmod_socket = 600 19 | chown_socket = www-data 20 | uid = www-data 21 | gid = www-data 22 | daemonize = /var/log/uwsgi.log 23 | log-reopen = 1 24 | 25 | # Process handling: don't overload the server 26 | processes = 10 27 | master = 1 28 | harakiri = 60 29 | max-requests = 20 30 | reload-on-as = 1024 31 | auto-procname = 1 32 | procname-prefix-spaced = goblet 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make35 | Git is a free and open source distributed 36 | version control system designed to handle everything from small to very large 37 | projects with speed and efficiency. 38 |
39 |40 | Goblet is a fast web-based git repository browser using libgit2. 41 |
42 | """ 43 | 44 | class Goblet(Flask): 45 | def __call__(self, environ, start_response): 46 | def x_accel_start_response(status, headers, exc_info=None): 47 | if self.config['USE_X_ACCEL_REDIRECT']: 48 | for num, (header, value) in enumerate(headers): 49 | if header == 'X-Sendfile': 50 | fn = value[value.rfind('/')+1:] 51 | if os.path.exists(os.path.join(self.config['CACHE_ROOT'], fn)): 52 | headers[num] = ('X-Accel-Redirect', '/snapshots/' + fn) 53 | break 54 | return start_response(status, headers, exc_info) 55 | return super(Goblet, self).__call__(environ, x_accel_start_response) 56 | 57 | app = Goblet(__name__) 58 | app.config.from_object(Defaults) 59 | if 'GOBLET_SETTINGS' in os.environ: 60 | app.config.from_envvar("GOBLET_SETTINGS") 61 | 62 | app.template_folder = os.path.join('themes', app.config['THEME'], 'templates') 63 | app.static_folder = os.path.join('themes', app.config['THEME'], 'static') 64 | 65 | # Configure parts of flask/jinja 66 | goblet.filters.register_filters(app) 67 | @app.context_processor 68 | def inject_functions(): 69 | return { 70 | 'tree_link': v.tree_link, 71 | 'raw_link': v.raw_link, 72 | 'blame_link': v.blame_link, 73 | 'blob_link': v.blob_link, 74 | 'history_link': v.history_link, 75 | 'file_icon': v.file_icon, 76 | 'decode': decode, 77 | 'S_ISGITLNK': stat.S_ISGITLNK, 78 | } 79 | 80 | # URL structure 81 | app.add_url_rule('/', view_func=v.IndexView.as_view('index')) 82 | app.add_url_rule('/%s') % escape(long) 79 | 80 | @filter 81 | def acks(message): 82 | if '\n' not in message: 83 | return [] 84 | acks = defaultdict(list) 85 | for ack, who in re.findall(r'^([-a-z]+(?:-[a-z]+)*):(.+?)(?:<.*)?\n', message.split('\n', 1)[1], flags=re.MULTILINE|re.I): 86 | ack = ack.lower().replace('-', ' ') 87 | ack = ack[0].upper() + ack[1:] # Can't use title 88 | acks[ack].append(who.strip()) 89 | return sorted(acks.items()) 90 | 91 | @filter 92 | def strftime(timestamp, format): 93 | return time.strftime(format, time.gmtime(timestamp)) 94 | 95 | @filter 96 | def decode(data): 97 | return decode_(data) 98 | 99 | @filter 100 | def ornull(data): 101 | if isinstance(data, list): 102 | for d in data: 103 | if not isinstance(d, Undefined): 104 | data = d 105 | break 106 | else: 107 | return 'null' 108 | if isinstance(data, Undefined): 109 | return 'null' 110 | for attr in ('name', 'hex'): 111 | data = getattr(data, attr, data) 112 | return Markup('"%s"') % data 113 | 114 | @filter 115 | def highlight(data, search): 116 | return Markup(data).replace(Markup(search), Markup('%s' % Markup(search))) 117 | 118 | @filter 119 | def dlength(diff): 120 | return len(list(diff)) 121 | 122 | def register_filters(app): 123 | app.jinja_env.filters.update(filters) 124 | -------------------------------------------------------------------------------- /goblet/json_views.py: -------------------------------------------------------------------------------- 1 | # Goblet - Web based git repository browser 2 | # Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | # See the LICENSE file for licensing details 4 | 5 | from goblet.views import PathView 6 | from goblet.filters import shortmsg 7 | from jinja2 import escape 8 | import json 9 | from flask import send_file, request, redirect, config, current_app 10 | import os 11 | 12 | class TreeChangedView(PathView): 13 | def handle_request(self, repo, path): 14 | ref, path, tree, _ = self.split_ref(repo, path) 15 | try: 16 | if ref not in repo: 17 | ref = repo.lookup_reference('refs/heads/%s' % ref).target.hex 18 | except ValueError: 19 | ref = repo.lookup_reference('refs/heads/%s' % ref).target.hex 20 | if hasattr(repo[ref], 'target'): 21 | ref = repo[repo[ref].target].hex 22 | cfile = os.path.join(repo.cpath, 'dirlog_%s_%s.json' % (ref, path.replace('/', '_'))) 23 | if not os.path.exists(cfile): 24 | tree = repo[ref].tree 25 | for elt in path.split('/'): 26 | if elt: 27 | tree = repo[tree[elt].hex] 28 | lastchanged = repo.tree_lastchanged(repo[ref], path) 29 | commits = {} 30 | for commit in set(lastchanged.values()): 31 | commit = repo[commit] 32 | commits[commit.hex] = [commit.commit_time, escape(shortmsg(commit.message))] 33 | for file in lastchanged: 34 | lastchanged[file] = (lastchanged[file], tree[file].hex[:7]) 35 | ret = {'files': lastchanged, 'commits': commits} 36 | if not current_app.config['TESTING']: 37 | with open(cfile, 'w') as fd: 38 | json.dump(ret, fd) 39 | if current_app.config['TESTING']: 40 | # When testing, we're not writing to the file, so we can't send_file or redirect 41 | return json.dumps(ret) 42 | elif 'wsgi.version' in request.environ and request.environ['SERVER_PORT'] != '5000': 43 | # Redirect to the file, let the webserver deal with it 44 | return redirect(cfile.replace(current_app.config['REPO_ROOT'], '')) 45 | else: 46 | return send_file(cfile) 47 | -------------------------------------------------------------------------------- /goblet/memoize.py: -------------------------------------------------------------------------------- 1 | # Goblet - Web based git repository browser 2 | # Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | # See the LICENSE file for licensing details 4 | 5 | class memoize: 6 | def __init__(self, function): 7 | self.function = function 8 | self.memoized = {} 9 | 10 | def __call__(self, *args): 11 | args_ = args 12 | if args and hasattr(args[0], 'path'): 13 | args_ = (args[0].path,) + args[1:] 14 | try: 15 | return self.memoized[args_] 16 | except KeyError: 17 | self.memoized[args_] = self.function(*args) 18 | return self.memoized[args_] 19 | -------------------------------------------------------------------------------- /goblet/monkey.py: -------------------------------------------------------------------------------- 1 | # Goblet - Web based git repository browser 2 | # Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | # See the LICENSE file for licensing details 4 | 5 | import os 6 | import pygit2 7 | from flask import current_app 8 | from memoize import memoize 9 | import pwd 10 | import pygments.lexers 11 | import stat 12 | from whelk import shell 13 | from goblet.encoding import decode 14 | from collections import defaultdict 15 | 16 | class Repository(pygit2.Repository): 17 | def __init__(self, path): 18 | if os.path.exists(path): 19 | super(Repository, self).__init__(path) 20 | else: 21 | super(Repository, self).__init__(path + '.git') 22 | self.gpath = os.path.join(self.path, 'goblet') 23 | self.cpath = os.path.join(self.gpath, 'cache') 24 | if not os.path.exists(self.gpath): 25 | os.mkdir(self.gpath) 26 | if not os.path.exists(self.cpath): 27 | os.mkdir(self.cpath) 28 | 29 | @memoize 30 | def get_description(self): 31 | desc = os.path.join(self.path, 'description') 32 | if not os.path.exists(desc): 33 | return "" 34 | with open(desc) as fd: 35 | return decode(fd.read()) 36 | description = property(get_description) 37 | 38 | @memoize 39 | def get_name(self): 40 | name = self.path.replace(current_app.config['REPO_ROOT'], '') 41 | if name.startswith('/'): 42 | name = name[1:] 43 | if name.endswith('/.git/'): 44 | name = name[:-6] 45 | else: 46 | name = name[:-5] 47 | return name 48 | name = property(get_name) 49 | 50 | @memoize 51 | def get_clone_urls(self): 52 | clone_base = current_app.config.get('CLONE_URLS_BASE', {}) 53 | repo_root = current_app.config['REPO_ROOT'] 54 | ret = {} 55 | for proto in ('git', 'ssh', 'http'): 56 | try: 57 | ret[proto] = self.config['goblet.cloneurl%s' % proto] 58 | continue 59 | except KeyError: 60 | pass 61 | if proto not in clone_base: 62 | continue 63 | if self.config['core.bare']: 64 | ret[proto] = clone_base[proto] + self.path.replace(repo_root, '') 65 | else: 66 | ret[proto] = clone_base[proto] + os.path.dirname(os.path.dirname(self.path)).replace(repo_root, '') 67 | return ret 68 | clone_urls = property(get_clone_urls) 69 | 70 | @memoize 71 | def get_owner(self): 72 | try: 73 | return self.config['goblet.owner'] 74 | except KeyError: 75 | uid = os.stat(self.path).st_uid 76 | pwn = pwd.getpwuid(uid) 77 | if pwn.pw_gecos: 78 | if ',' in pwn.pw_gecos: 79 | return pwn.pw_gecos[:pwn.pw_gecos.find(',')] 80 | return pwn.pw_gecos 81 | return pwn.pw_name 82 | owner = property(get_owner) 83 | 84 | def branches(self): 85 | return sorted([x[11:] for x in self.listall_references() if x.startswith('refs/heads/')]) 86 | 87 | def tags(self): 88 | return sorted([x[10:] for x in self.listall_references() if x.startswith('refs/tags/')]) 89 | 90 | @memoize 91 | def get_reverse_refs(self): 92 | ret = defaultdict(list) 93 | for ref in self.listall_references(): 94 | if ref.startswith('refs/remotes/'): 95 | continue 96 | if ref.startswith('refs/tags/'): 97 | obj = self[self.lookup_reference(ref).target.hex] 98 | if obj.type == pygit2.GIT_OBJ_COMMIT: 99 | ret[obj.hex].append(('tag', ref[10:])) 100 | else: 101 | ret[self[self[obj.target].hex].hex].append(('tag', ref[10:])) 102 | else: 103 | ret[self.lookup_reference(ref).target.hex].append(('head', ref[11:])) 104 | return ret 105 | reverse_refs = property(get_reverse_refs) 106 | 107 | def ref_for_commit(self, hex): 108 | if hasattr(hex, 'hex'): 109 | hex = hex.hex 110 | refs = self.reverse_refs.get(hex, None) 111 | if not refs: 112 | return hex 113 | return refs[-1][1] 114 | 115 | @property 116 | def head(self): 117 | try: 118 | return super(Repository, self).head 119 | except pygit2.GitError: 120 | return None 121 | 122 | def get_commits(self, ref, skip, count, search=None, file=None): 123 | num = 0 124 | path = [] 125 | if file: 126 | path = file.split('/') 127 | for commit in self.walk(ref.hex, pygit2.GIT_SORT_TIME): 128 | if search and search not in commit.message: 129 | continue 130 | if path: 131 | in_current = found_same = in_parent = False 132 | try: 133 | tree = commit.tree 134 | for file in path[:-1]: 135 | tree = self[tree[file].hex] 136 | if not isinstance(tree, pygit2.Tree): 137 | raise KeyError(file) 138 | oid = tree[path[-1]].oid 139 | in_current = True 140 | except KeyError: 141 | pass 142 | try: 143 | for parent in commit.parents: 144 | tree = parent.tree 145 | for file in path[:-1]: 146 | tree = self[tree[file].hex] 147 | if not isinstance(tree, pygit2.Tree): 148 | raise KeyError(file) 149 | if tree[path[-1]].oid == oid: 150 | in_parent = found_same = True 151 | break 152 | in_parent = True 153 | except KeyError: 154 | pass 155 | if not in_current and not in_parent: 156 | continue 157 | if found_same: 158 | continue 159 | 160 | num += 1 161 | if num < skip: 162 | continue 163 | if num >= skip + count: 164 | break 165 | yield commit 166 | 167 | def describe(self, commit): 168 | tags = [self.lookup_reference(x) for x in self.listall_references() if x.startswith('refs/tags')] 169 | if not tags: 170 | return 'g' + commit[:7] 171 | tags = [(tag.name[10:], self[tag.target.hex]) for tag in tags] 172 | tags = dict([(hasattr(obj, 'target') and self[obj.target].hex or obj.hex, name) for name, obj in tags]) 173 | count = 0 174 | for parent in self.walk(commit, pygit2.GIT_SORT_TIME): 175 | if parent.hex in tags: 176 | if count == 0: 177 | return tags[parent.hex] 178 | return '%s-%d-g%s' % (tags[parent.hex], count, commit[:7]) 179 | count += 1 180 | return 'g' + commit[:7] 181 | 182 | def ls_tree(self, tree, path=''): 183 | ret = [] 184 | for entry in tree: 185 | if stat.S_ISDIR(entry.filemode): 186 | ret += self.ls_tree(repo[entry.hex], os.path.join(path, entry.name)) 187 | else: 188 | ret.append(os.path.join(path, entry.name)) 189 | return ret 190 | 191 | def tree_lastchanged(self, commit, path): 192 | """Get a dict of {name: hex} for commits that last changed files in a directory""" 193 | data = self.git('blame-tree', '--max-depth=0', commit.hex, '--', os.path.join('.', path) + os.sep).stdout 194 | data = data.decode('utf-8').splitlines() 195 | if not data: 196 | raise ValueError("Empty blame-tree output") 197 | data = [x.split(None, 1) for x in data] 198 | if path: 199 | data = [(p[p.rfind('/')+1:], m) for (m,p) in data] 200 | else: 201 | data = [(p, m) for (m,p) in data] 202 | return dict(data) 203 | 204 | def blame(self, commit, path): 205 | if hasattr(commit, 'hex'): 206 | commit = commit.hex 207 | contents = decode(self.git('blame', '-p', commit, '--', path).stdout).split('\n') 208 | contents.pop(-1) 209 | commits = {} 210 | last_commit = None 211 | lines = [] 212 | orig_line = line_now = 0 213 | for line in contents: 214 | if not last_commit: 215 | last_commit, orig_line, line_now = line.split()[:3] 216 | if last_commit not in commits: 217 | commits[last_commit] = {'hex': last_commit} 218 | elif line.startswith('\t'): 219 | lines.append((line[1:], orig_line, line_now, commits[last_commit])) 220 | last_commit = None 221 | elif line == 'boundary': 222 | commits[last_commit]['previous'] = None 223 | else: 224 | key, val = line.split(None, 1) 225 | commits[last_commit][key] = val 226 | return lines 227 | 228 | def grep(self, commit, path, query): 229 | if hasattr(commit, 'hex'): 230 | commit = commit.hex 231 | results = self.git('grep', '-n', '--full-name', '-z', '-I', '-C1', '--heading', '--break', query, commit, '--', path).stdout.strip() 232 | if not results: 233 | raise StopIteration 234 | files = results.split('\n\n') 235 | for file in files: 236 | chunks = [] 237 | for chunk in [x.split('\n') for x in file.split('\n--\n')]: 238 | chunks.append([line.split('\0') for line in chunk]) 239 | filename = chunks[0].pop(0)[0].split(':', 1) 240 | yield filename, chunks 241 | 242 | def git(self, *args): 243 | return shell.git('--git-dir', self.path, '--work-tree', self.workdir or '/nonexistent', *args) 244 | 245 | def get_tree(tree, path): 246 | for dir in path: 247 | if dir not in tree: 248 | return None 249 | tree = repo[tree[dir].hex] 250 | return tree 251 | 252 | pygit2.Repository = Repository 253 | 254 | def S_ISGITLNK(mode): 255 | return (mode & 0160000) == 0160000 256 | stat.S_ISGITLNK = S_ISGITLNK 257 | 258 | # Let's detect .pl as perl instead of prolog 259 | pygments.lexers.LEXERS['PrologLexer'] = ('pygments.lexers.compiled', 'Prolog', ('prolog',), ('*.prolog', '*.pro'), ('text/x-prolog',)) 260 | -------------------------------------------------------------------------------- /goblet/render.py: -------------------------------------------------------------------------------- 1 | # Goblet - Web based git repository browser 2 | # Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | # See the LICENSE file for licensing details 4 | 5 | from flask import url_for 6 | from jinja2 import Markup, escape 7 | import pygments 8 | import pygments.formatters 9 | import pygments.lexers 10 | from goblet.encoding import decode 11 | from whelk import shell 12 | 13 | import re 14 | import markdown as markdown_ 15 | import docutils.core 16 | import time 17 | 18 | renderers = {} 19 | image_exts = ('.gif', '.png', '.bmp', '.tif', '.tiff', '.jpg', '.jpeg', '.ppm', 20 | '.pnm', '.pbm', '.pgm', '.webp', '.ico') 21 | 22 | def render(repo, ref, path, entry, plain=False, blame=False): 23 | renderer = detect_renderer(repo, entry) 24 | if plain: 25 | if renderer[0] in ('rest', 'markdown'): 26 | renderer = ('code', pygments.lexers.get_lexer_for_filename(path)) 27 | elif renderer[0] == 'man': 28 | renderer = ('code', pygments.lexers.get_lexer_by_name('groff')) 29 | if blame: 30 | if renderer[0] in ('rest', 'markdown'): 31 | renderer = ('code', pygments.lexers.get_lexer_for_filename(path), None, True) 32 | elif renderer[0] == 'man': 33 | renderer = ('code', pygments.lexers.get_lexer_by_name('groff'), None, True) 34 | elif renderer[0] == 'code': 35 | renderer = list(renderer[:2]) + [None, True] 36 | return renderer[0], renderers[renderer[0]](repo, ref, path, entry, *renderer[1:]) 37 | 38 | def detect_renderer(repo, entry): 39 | name = entry.name.lower() 40 | ext = name[name.rfind('.'):] 41 | # First: filename to detect images 42 | if ext in image_exts: 43 | return 'image', 44 | # Known formatters 45 | if ext in ('.rst', '.rest'): 46 | return 'rest', 47 | if ext == '.md': 48 | return 'markdown', 49 | if re.match('^\.[1-8](?:fun|p|posix|ssl|perl|pm|gcc|snmp)?$', ext): 50 | return 'man', 51 | # Try pygments 52 | try: 53 | lexer = pygments.lexers.get_lexer_for_filename(name) 54 | return 'code', lexer 55 | except pygments.util.ClassNotFound: 56 | pass 57 | 58 | obj = repo[entry.oid] 59 | if obj.size > 1024*1024*5: 60 | return 'binary', 61 | data = obj.data 62 | 63 | if data.startswith('#!'): 64 | shbang = data[:data.find('\n')] 65 | # Needs to match: 66 | # #!python 67 | # #!/path/to/python 68 | # #!/path/to/my-python 69 | # #!/path/to/python2.7 70 | # And any permutation of those features 71 | # path prefix interp version 72 | shbang = re.match(r'#!(?:\S*/)?(?:\S*-)?([^0-9 ]*)(?:\d.*)?', shbang).group(1) 73 | # Fixers 74 | shbang = { 75 | 'sh': 'bash', 76 | 'ksh': 'bash', 77 | 'zsh': 'bash', 78 | 'node': 'javascript', 79 | }.get(shbang, shbang) 80 | lex = pygments.lexers.find_lexer_class(shbang.title()) 81 | if lex: 82 | return 'code', lex() 83 | 84 | if '\0' in data: 85 | return 'binary', 86 | 87 | return 'code', pygments.lexers.TextLexer(), data 88 | 89 | def renderer(func): 90 | renderers[func.__name__] = func 91 | return func 92 | 93 | @renderer 94 | def image(repo, ref, path, entry): 95 | return Markup("
%s" % data) 102 | 103 | @renderer 104 | def code(repo, ref, path, entry, lexer, data=None, blame=False): 105 | from goblet.views import blob_link 106 | try: 107 | data = decode(data or repo[entry.oid].data) 108 | except: 109 | data = '(Binary data)' 110 | formatter = pygments.formatters.html.HtmlFormatter(linenos='inline', linenospecial=10, encoding='utf-8', anchorlinenos=True, lineanchors='l') 111 | html = Markup(pygments.highlight(data, lexer, formatter).decode('utf-8')) 112 | if blame: 113 | blame = repo.blame(ref, path) 114 | if not blame: 115 | return 116 | blame.append(None) 117 | def replace(match): 118 | line = int(match.group(2)) - 1 119 | _, orig_line, _, commit = blame[line] 120 | link = blob_link(repo, commit['hex'], path) 121 | if blame[-1] == commit['hex']: 122 | return Markup(' %s%s' % (match.group(1), link, orig_line, match.group(2))) 123 | link2 = url_for('commit', repo=repo.name, ref=commit['hex']) 124 | blame[-1] = commit['hex'] 125 | return Markup('%s %s%s' % (link2, commit['summary'], 126 | time.strftime('%Y-%m-%d', time.gmtime(int(commit['committer-time']))), 127 | commit['hex'][:7], match.group(1), link, orig_line, match.group(2))) 128 | html = re.sub(r'(\s*)(\d+)', replace, html) 129 | return html 130 | 131 | add_plain_link = Markup('''''') 132 | @renderer 133 | def markdown(repo, ref, path, entry): 134 | data = decode(repo[entry.oid].data) 135 | return Markup(markdown_.Markdown(safe_mode="escape").convert(data)) + add_plain_link 136 | 137 | @renderer 138 | def rest(repo, ref, path, entry): 139 | data = decode(repo[entry.oid].data) 140 | settings = { 141 | 'file_insertion_enabled': False, 142 | 'raw_enabled': False, 143 | 'output_encoding': 'utf-8', 144 | 'report_level': 5, 145 | } 146 | data = docutils.core.publish_parts(data,settings_overrides=settings,writer_name='html') 147 | return Markup(data['body']) + add_plain_link 148 | 149 | @renderer 150 | def man(repo, ref, path, entry): 151 | res = shell.groff('-Thtml', '-P', '-l', '-mandoc', input=repo[entry.oid].data) 152 | if res.returncode != 0: 153 | raise RuntimeError(res.stderr) 154 | data = decode(res.stdout) 155 | return Markup(data[data.find('')+6:data.find('')]) + add_plain_link 156 | 157 | @renderer 158 | def binary(repo, ref, path, entry): 159 | return 'Binary file, %d bytes' % repo[entry.oid].size 160 | -------------------------------------------------------------------------------- /goblet/themes/default/static/body.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/body.jpg -------------------------------------------------------------------------------- /goblet/themes/default/static/chosen: -------------------------------------------------------------------------------- 1 | ../../../../chosen/chosen/ -------------------------------------------------------------------------------- /goblet/themes/default/static/chosen.css: -------------------------------------------------------------------------------- 1 | /* 2 | Goblet - Web based git repository browser 3 | Copyright (C) 2012-2014 Dennis Kaarsemaker 4 | See the LICENSE file for licensing details 5 | */ 6 | 7 | /* @group Base */ 8 | .chzn-container { 9 | position: relative; 10 | display: inline-block; 11 | width: 100%; 12 | height: 100%; 13 | margin-bottom: -5px; 14 | } 15 | 16 | .chzn-container .chzn-drop { 17 | background: #fff; 18 | border: 1px solid rgb(226, 224, 216); 19 | border-top: 0; 20 | position: absolute; 21 | top: 29px; 22 | left: 0; 23 | z-index: 1010; 24 | } 25 | /* @end */ 26 | 27 | /* @group Single Chosen */ 28 | .chzn-container-single .chzn-single { 29 | border-radius: 5px; 30 | border: 1px solid rgb(226, 224, 216); 31 | display: block; 32 | position: relative; 33 | padding: 4px 8px; 34 | text-decoration: none; 35 | } 36 | .chzn-container-single .chzn-single span { 37 | margin-right: 26px; 38 | display: block; 39 | overflow: hidden; 40 | white-space: nowrap; 41 | -o-text-overflow: ellipsis; 42 | -ms-text-overflow: ellipsis; 43 | text-overflow: ellipsis; 44 | } 45 | .chzn-container-single .chzn-single abbr { 46 | display: block; 47 | position: absolute; 48 | right: 26px; 49 | top: 6px; 50 | width: 12px; 51 | height: 13px; 52 | font-size: 1px; 53 | } 54 | .chzn-container-single .chzn-single abbr:hover { 55 | background-position: right -11px; 56 | } 57 | .chzn-container-single.chzn-disabled .chzn-single abbr:hover { 58 | background-position: right top; 59 | } 60 | .chzn-container-single .chzn-single div { 61 | position: absolute; 62 | right: 0; 63 | top: 0; 64 | display: block; 65 | height: 100%; 66 | width: 18px; 67 | } 68 | .chzn-container-single .chzn-single div b { 69 | background: url('chosen/chosen-sprite.png') no-repeat 0 4px; 70 | display: block; 71 | width: 100%; 72 | height: 100%; 73 | } 74 | .chzn-container-single .chzn-search { 75 | padding: 3px 4px; 76 | position: relative; 77 | margin: 0; 78 | white-space: nowrap; 79 | z-index: 1010; 80 | } 81 | .chzn-container-single .chzn-search input { 82 | background: #fff url('chosen/chosen-sprite.png') no-repeat 100% -22px; 83 | margin: 1px 0; 84 | padding: 4px 20px 4px 5px; 85 | outline: 0; 86 | border: 1px solid rgb(226, 224, 216); 87 | font-family: sans-serif; 88 | font-size: 1em; 89 | } 90 | .chzn-container-single .chzn-drop { 91 | -webkit-border-radius: 0 0 4px 4px; 92 | -moz-border-radius : 0 0 4px 4px; 93 | border-radius : 0 0 4px 4px; 94 | } 95 | /* @end */ 96 | 97 | .chzn-container-single-nosearch .chzn-search input { 98 | position: absolute; 99 | left: -9000px; 100 | } 101 | 102 | /* @group Multi Chosen */ 103 | .chzn-container-multi .chzn-choices { 104 | border: 1px solid rgb(226, 224, 216); 105 | margin: 0; 106 | padding: 0; 107 | cursor: text; 108 | overflow: hidden; 109 | height: auto !important; 110 | height: 1%; 111 | position: relative; 112 | } 113 | .chzn-container-multi .chzn-choices li { 114 | float: left; 115 | list-style: none; 116 | } 117 | .chzn-container-multi .chzn-choices .search-field { 118 | white-space: nowrap; 119 | margin: 0; 120 | padding: 0; 121 | } 122 | .chzn-container-multi .chzn-choices .search-field input { 123 | background: transparent !important; 124 | border: 0 !important; 125 | font-size: 100%; 126 | height: 15px; 127 | padding: 5px; 128 | margin: 1px 0; 129 | outline: 0; 130 | -webkit-box-shadow: none; 131 | -moz-box-shadow : none; 132 | box-shadow : none; 133 | } 134 | .chzn-container-multi .chzn-choices .search-choice { 135 | -webkit-border-radius: 3px; 136 | -moz-border-radius : 3px; 137 | border-radius : 3px; 138 | border: 1px solid rgb(226, 224, 216); 139 | line-height: 13px; 140 | padding: 3px 20px 3px 5px; 141 | margin: 3px 0 3px 5px; 142 | position: relative; 143 | cursor: default; 144 | } 145 | .chzn-container-multi .chzn-choices .search-choice.search-choice-disabled { 146 | background-color: #e4e4e4; 147 | border: 1px solid #cccccc; 148 | padding-right: 5px; 149 | } 150 | .chzn-container-multi .chzn-choices .search-choice-focus { 151 | background: #d4d4d4; 152 | } 153 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close { 154 | display: block; 155 | position: absolute; 156 | right: 3px; 157 | top: 4px; 158 | width: 12px; 159 | height: 13px; 160 | font-size: 1px; 161 | background: url('chosen/chosen-sprite.png') right top no-repeat; 162 | } 163 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { 164 | background-position: right -11px; 165 | } 166 | .chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { 167 | background-position: right -11px; 168 | } 169 | /* @end */ 170 | 171 | /* @group Results */ 172 | .chzn-container .chzn-results { 173 | max-height: 240px; 174 | padding: 0 0 0 4px; 175 | position: relative; 176 | overflow-x: hidden; 177 | overflow-y: auto; 178 | -webkit-overflow-scrolling: touch; 179 | } 180 | .chzn-container-multi .chzn-results { 181 | margin: -1px 0 0; 182 | padding: 0; 183 | } 184 | .chzn-container .chzn-results li { 185 | display: none; 186 | line-height: 15px; 187 | padding: 5px 6px; 188 | margin: 0; 189 | list-style: none; 190 | } 191 | .chzn-container .chzn-results .active-result { 192 | cursor: pointer; 193 | display: list-item; 194 | } 195 | .chzn-container .chzn-results .highlighted { 196 | background-color: rgb(3, 136, 166); 197 | color: #fff; 198 | } 199 | .chzn-container .chzn-results li em { 200 | background: #feffde; 201 | font-style: normal; 202 | } 203 | .chzn-container .chzn-results .highlighted em { 204 | background: transparent; 205 | } 206 | .chzn-container .chzn-results .no-results { 207 | background: #f4f4f4; 208 | display: list-item; 209 | } 210 | .chzn-container .chzn-results .group-result { 211 | cursor: default; 212 | color: #999; 213 | font-weight: bold; 214 | } 215 | .chzn-container .chzn-results .group-option { 216 | padding-left: 15px; 217 | } 218 | .chzn-container-multi .chzn-drop .result-selected { 219 | display: none; 220 | } 221 | .chzn-container .chzn-results-scroll { 222 | background: white; 223 | margin: 0 4px; 224 | position: absolute; 225 | text-align: center; 226 | width: 321px; /* This should by dynamic with js */ 227 | z-index: 1; 228 | } 229 | .chzn-container .chzn-results-scroll span { 230 | display: inline-block; 231 | height: 17px; 232 | text-indent: -5000px; 233 | width: 9px; 234 | } 235 | .chzn-container .chzn-results-scroll-down { 236 | bottom: 0; 237 | } 238 | .chzn-container .chzn-results-scroll-down span { 239 | background: url('chosen/chosen-sprite.png') no-repeat -4px -3px; 240 | } 241 | .chzn-container .chzn-results-scroll-up span { 242 | background: url('chosen/chosen-sprite.png') no-repeat -22px -3px; 243 | } 244 | /* @end */ 245 | 246 | /* @group Active */ 247 | .chzn-container-active .chzn-single { 248 | border: 1px solid #5897fb; 249 | } 250 | .chzn-container-active .chzn-single-with-drop { 251 | border: 1px solid rgb(226, 224, 216); 252 | border-bottom-left-radius : 0; 253 | border-bottom-right-radius: 0; 254 | } 255 | .chzn-container-active .chzn-single-with-drop div { 256 | background: transparent; 257 | border-left: none; 258 | } 259 | .chzn-container-active .chzn-single-with-drop div b { 260 | background-position: -18px 1px; 261 | } 262 | .chzn-container-active .chzn-choices { 263 | border: 1px solid #5897fb; 264 | } 265 | /* @end */ 266 | 267 | /* @group Disabled Support */ 268 | .chzn-disabled { 269 | cursor: default; 270 | opacity:0.5 !important; 271 | } 272 | .chzn-disabled .chzn-single { 273 | cursor: default; 274 | } 275 | .chzn-disabled .chzn-choices .search-choice .search-choice-close { 276 | cursor: default; 277 | } 278 | 279 | /* @group Right to Left */ 280 | .chzn-rtl { text-align: right; } 281 | .chzn-rtl .chzn-single { padding: 0 8px 0 0; overflow: visible; } 282 | .chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; direction: rtl; } 283 | 284 | .chzn-rtl .chzn-single div { left: 3px; right: auto; } 285 | .chzn-rtl .chzn-single abbr { 286 | left: 26px; 287 | right: auto; 288 | } 289 | .chzn-rtl .chzn-choices .search-field input { direction: rtl; } 290 | .chzn-rtl .chzn-choices li { float: right; } 291 | .chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; } 292 | .chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; background-position: right top;} 293 | .chzn-rtl.chzn-container-single .chzn-results { margin: 0 0 4px 4px; padding: 0 4px 0 0; } 294 | .chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 15px; } 295 | .chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; } 296 | .chzn-rtl .chzn-search input { 297 | background: #fff url('chosen/chosen-sprite.png') no-repeat -38px -22px; 298 | background: url('chosen/chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); 299 | background: url('chosen/chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 300 | background: url('chosen/chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 301 | background: url('chosen/chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 302 | background: url('chosen/chosen-sprite.png') no-repeat -38px -22px, linear-gradient(#eeeeee 1%, #ffffff 15%); 303 | padding: 4px 5px 4px 20px; 304 | direction: rtl; 305 | } 306 | /* @end */ 307 | -------------------------------------------------------------------------------- /goblet/themes/default/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/favicon.ico -------------------------------------------------------------------------------- /goblet/themes/default/static/file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/file_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/static/folder_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/folder_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/static/goblet.css: -------------------------------------------------------------------------------- 1 | /* 2 | Goblet - Web based git repository browser 3 | Copyright (C) 2012-2014 Dennis Kaarsemaker 4 | See the LICENSE file for licensing details 5 | */ 6 | 7 | .goblet { 8 | background-color: #fcfcfa; 9 | color: rgb(78, 68, 60); 10 | background: url("/static/body.jpg") repeat scroll 0% 0% rgb(240, 239, 231); 11 | font-family: "Lato","Helvetica Neue",sans-serif; 12 | font-size: 14px; 13 | padding: 5px; 14 | margin: 0px; 15 | } 16 | .goblet * { 17 | padding: 0px; 18 | margin: 0px; 19 | } 20 | .goblet #search { 21 | position: absolute; 22 | z-index: 1000; 23 | right: 5px; 24 | top: 10px; 25 | width: 262px; 26 | padding-left: 32px; 27 | background: url("/static/search.png") no-repeat scroll 10px 50% rgb(252, 252, 250); 28 | border: 1px solid rgb(206, 204, 197); 29 | border-radius: 20px 20px 20px 20px; 30 | box-shadow: 0px 1px 4px rgb(221, 221, 221) inset; 31 | } 32 | .goblet #search input { 33 | border-radius: 20px 20px 20px 20px; 34 | border: medium none; 35 | margin-top: 5px; 36 | margin-bottom: 1px; 37 | line-height: 1em; 38 | width: 100%; 39 | height: 20px; 40 | background-color: transparent; 41 | } 42 | 43 | .goblet input:-moz-placeholder { 44 | color: rgb(154, 153, 148) !important; 45 | } 46 | 47 | .goblet a, .goblet a:hover, .goblet a:active { 48 | color: rgb(3, 136, 166); 49 | text-decoration: none; 50 | } 51 | 52 | .goblet #content { 53 | background-color: white; 54 | border-radius: 10px; 55 | border: 1px solid rgb(226, 224, 216); 56 | padding: 0.3em 1em; 57 | margin: 0; 58 | } 59 | .goblet h1 { 60 | font-size: 28px; 61 | line-height: 44px; 62 | margin-bottom: 0.4em; 63 | } 64 | .goblet h2 { 65 | font-size: 18px; 66 | line-height: 36px; 67 | margin-bottom: 0.3em; 68 | font-weight: bold; 69 | } 70 | .goblet h1, .goblet h2, .goblet h3, .goblet h4, .goblet h5, .goblet h6 { 71 | margin: 0px; 72 | } 73 | .goblet .about { 74 | float: right; 75 | width: 300px; 76 | } 77 | .goblet .repo { 78 | border-top: 1px solid rgb(226, 224, 216); 79 | padding: 0 1em 1em 1em; 80 | margin-bottom: 0.2em; 81 | width: 600px; 82 | } 83 | .goblet .owner { 84 | color: rgb(154, 153, 148); 85 | font-size: 50%; 86 | } 87 | .goblet .lastchange { 88 | color: rgb(154, 153, 148); 89 | font-size: 75%; 90 | } 91 | .goblet .footer { 92 | color: rgb(154, 153, 148); 93 | text-align: center; 94 | font-size: 75%; 95 | font-weight: bold; 96 | } 97 | .goblet .footer a { 98 | color: rgb(154, 153, 148); 99 | } 100 | .goblet .tabnav { 101 | border-bottom: solid 1px rgb(226, 224, 216); 102 | margin: 10px 0px; 103 | padding: 0px; 104 | } 105 | .goblet .tabnav-tabs { 106 | display: inline-block; 107 | margin: 4px 8px 4px 0px; 108 | } 109 | .goblet .tabnav-tabs > li { 110 | display: inline-block; 111 | } 112 | .goblet .tabnav-tabs a { 113 | border-color: rgb(226, 224, 216); 114 | border-radius: 3px 3px 0px 0px; 115 | color: rgb(78, 68, 60); 116 | background-color: white; 117 | } 118 | .goblet .tabnav-tab { 119 | border-width: 1px 1px 0px; 120 | border-style: solid solid none; 121 | padding: 4px 8px; 122 | margin: 0px 10px; 123 | background-color: white; 124 | } 125 | .goblet .tabnav-tabs .selected { 126 | font-weight: bold; 127 | padding-bottom: 5px; 128 | background-color: white; 129 | } 130 | .nocommit { 131 | margin: 100px; 132 | text-align: center; 133 | font-size: 150%; 134 | color: rgb(226, 224, 216); 135 | } 136 | .goblet #filetree { 137 | width: 100%; 138 | } 139 | .goblet #filetree th, .goblet .diffstat th { 140 | text-align: left; 141 | } 142 | .goblet #filetree .name, .goblet #filetree .age { 143 | white-space: nowrap; 144 | padding-right: 15px; 145 | padding-left: 10px; 146 | } 147 | .goblet #filetree .message { 148 | width: 99%; 149 | } 150 | .goblet #filetree img { 151 | vertical-align: middle; 152 | } 153 | .goblet #filetree td { 154 | padding: 2px; 155 | } 156 | .goblet .blob { 157 | border: solid 1px rgb(226, 224, 216); 158 | border-radius: 5px; 159 | margin: 1em 0px; 160 | } 161 | 162 | .goblet .blob-inner { 163 | padding: 10px; 164 | } 165 | 166 | .goblet .blob h2 { 167 | background-color: rgb(237, 235, 227); 168 | padding: 0px 10px; 169 | line-height: 30px; 170 | background-repeat: no-repeat; 171 | } 172 | 173 | .goblet .s_20 { 174 | width: 20px; 175 | height: 20px; 176 | } 177 | 178 | .goblet .s_36 { 179 | width: 36px; 180 | height: 36px; 181 | } 182 | 183 | .goblet .blob { 184 | overflow: auto; 185 | } 186 | .goblet .commit .gravatar { 187 | border-radius: 4px; 188 | float: left; 189 | margin-left: -44px; 190 | } 191 | .goblet .commit { 192 | border-bottom: solid 1px rgb(226, 224, 216); 193 | border-left: solid 1px rgb(226, 224, 216); 194 | border-right: solid 1px rgb(226, 224, 216); 195 | padding: 5px; 196 | overflow: auto; 197 | } 198 | .goblet .commitmsg { 199 | margin-left: 44px; 200 | } 201 | 202 | .goblet .commits .invisible { 203 | display: none; 204 | } 205 | 206 | .goblet .commitmsg a { 207 | color: rgb(78, 68, 60); 208 | font-weight: bold; 209 | } 210 | 211 | .goblet .commitmsg a:hover { 212 | color: rgb(78, 68, 60); 213 | font-weight: bold; 214 | text-decoration: underline; 215 | } 216 | .goblet .commitdata { 217 | padding-top: 1em; 218 | } 219 | .goblet .commitdata span, .goblet .name .submodule { 220 | color: rgb(154, 153, 148); 221 | font-size: 90%; 222 | } 223 | .goblet .commitmsg .committer { 224 | margin-left: 2em; 225 | } 226 | .goblet pre, .goblet code { 227 | font-family: 'Source Code Pro', monospace; 228 | font-size: 12px; 229 | color: rgb(51, 51, 51); 230 | } 231 | .goblet .show_long { 232 | color: rgb(154, 153, 148); 233 | } 234 | .goblet .show_long:hover { 235 | text-decoration: underline; 236 | } 237 | .goblet .highlight .lineno { color: rgb(154, 153, 148); } 238 | .goblet .highlight .special { color: rgb(78, 68, 60); } 239 | 240 | .goblet .commitdate { 241 | border: solid 1px rgb(226, 224, 216); 242 | border-bottom-width: 0px; 243 | font-weight: bold; 244 | border-radius: 5px 5px 0px 0px; 245 | padding: 3px 5px; 246 | font-size: 110%; 247 | background-color: rgb(237, 235, 227); 248 | margin-top: 10px; 249 | } 250 | .blobdiff { 251 | width: 100%; 252 | border-spacing: 0px; 253 | border-collapse: collapse; 254 | } 255 | .goblet .blobdiff td { 256 | font-size: 12px; 257 | padding: 0px 8px; 258 | line-height: 150%; 259 | } 260 | .goblet .blobdiff tr:hover, .goblet .blobdiff tr:hover * { 261 | background-color: rgb(255, 255, 204); 262 | } 263 | .goblet .deletion .diffcontent, .goblet .statbar { 264 | background-color: rgb(255, 221, 221); 265 | } 266 | .goblet .statbar { 267 | width: 100px; 268 | line-height: 8px; 269 | } 270 | .goblet .addition .diffcontent, .goblet .statbar div { 271 | background-color: rgb(221, 255, 221); 272 | } 273 | .goblet .blobdiff .lineno { 274 | background-color: rgb(237, 235, 227); 275 | width: 3em; 276 | } 277 | .goblet .pagelink-prev { 278 | padding: 8px 10px; 279 | background-color: rgb(237, 235, 227); 280 | line-height: 150%; 281 | border-radius: 5px 0 0 5px; 282 | } 283 | .goblet .pagelink-next { 284 | padding: 8px 10px; 285 | background-color: rgb(237, 235, 227); 286 | line-height: 150%; 287 | border-radius: 0 5px 5px 0; 288 | } 289 | .goblet .pagination { 290 | margin-top: 25px; 291 | margin-bottom: 25px; 292 | } 293 | .goblet pre.literal-block { 294 | padding: 5px; 295 | } 296 | .goblet .blob .actions, .goblet .commitdate .actions { 297 | display: inline-block; 298 | float: right; 299 | font-weight: normal; 300 | font-size: 13px; 301 | } 302 | .goblet .ref { 303 | padding: 0px 3px; 304 | border-radius: 3px; 305 | font-size: 80%; 306 | } 307 | .goblet .ref_head { 308 | background-color: rgb(221, 255, 221); 309 | } 310 | .goblet .ref_tag { 311 | background-color: rgb(255, 255, 204); 312 | } 313 | .goblet .lastchange img { 314 | border-radius: 3px; 315 | } 316 | .goblet .clonelinks { 317 | margin-top: 1em; 318 | } 319 | .goblet .clonelinks ul { 320 | display: inline; 321 | } 322 | .goblet .clonelinks li { 323 | display: inline; 324 | border-radius: 3px; 325 | color: rgb(78, 68, 60); 326 | font-weight: bold; 327 | background-color: rgb(237, 235, 227); 328 | padding: 0px 5px; 329 | margin: 2px; 330 | } 331 | .goblet .clonelinks li:hover { 332 | cursor: pointer; 333 | } 334 | .goblet .clonelinks li span { 335 | display: none; 336 | } 337 | .goblet .clonelinks input { 338 | border-color: rgb(226, 224, 216); 339 | border-width: 1px; 340 | border-radius: 3px; 341 | padding: 0px 10px; 342 | width: 400px; 343 | } 344 | .goblet .searchresult { 345 | font-weight: bold; 346 | color: rgb(241, 78, 50); 347 | } 348 | .goblet .parents { 349 | float: right; 350 | width: 50%; 351 | } 352 | .goblet .parents tr :first-child { 353 | width: 7em; 354 | vertical-align: top; 355 | padding-right: 0.5em; 356 | } 357 | .goblet .parents tr { 358 | overflow: hidden; 359 | height: 1em; 360 | } 361 | 362 | .goblet .render-markdown ul, .render-markdown ol, .render-markdown pre { 363 | margin-left: 2em; 364 | margin-top: 0.5em; 365 | } 366 | .goblet .render-markdown p { 367 | padding-top: 0.5em; 368 | } 369 | .goblet .render-markdown h1, 370 | .goblet .render-markdown h2, 371 | .goblet .render-markdown h3, 372 | .goblet .render-markdown h4, 373 | .goblet .render-markdown h5, 374 | .goblet .render-markdown h6 { 375 | background-color: #fff; 376 | padding: 0.5em 0px 0px 0px; 377 | 378 | } 379 | 380 | .goblet .render-rest ul, .render-rest ol, .render-rest pre { 381 | margin-left: 2em; 382 | margin-top: 0.5em; 383 | } 384 | .goblet .render-rest p { 385 | padding-top: 0.5em; 386 | } 387 | .goblet .render-rest h1, 388 | .goblet .render-rest h2, 389 | .goblet .render-rest h3, 390 | .goblet .render-rest h4, 391 | .goblet .render-rest h5, 392 | .goblet .render-rest h6 { 393 | background-color: #fff; 394 | padding: 0.5em 0px 0px 0px; 395 | 396 | } 397 | 398 | .goblet .render-man ul, .render-man ol, .render-man pre { 399 | margin-left: 2em; 400 | margin-top: 0.5em; 401 | } 402 | .goblet .render-man p { 403 | } 404 | .goblet .render-man h1, 405 | .goblet .render-man h2, 406 | .goblet .render-man h3, 407 | .goblet .render-man h4, 408 | .goblet .render-man h5, 409 | .goblet .render-man h6 { 410 | background-color: #fff; 411 | padding: 0.5em 0px 0px 0px; 412 | 413 | } 414 | .goblet .render-man hr { 415 | display: none; 416 | } 417 | -------------------------------------------------------------------------------- /goblet/themes/default/static/goblet.js: -------------------------------------------------------------------------------- 1 | function load_tree_log() { 2 | var logurl = '/j/' + repo + '/treechanged/' + (ref ? ref + '/' : '' ) + (path ? path + '/' : ''); 3 | $.getJSON(logurl, success=function(data) { 4 | $.each(data.files, function(file, id) { 5 | $('#age_' + id[1]).html(humantime(data.commits[id[0]][0])); 6 | $('#msg_' + id[1]).html('' + data.commits[id[0]][1] + ''); 7 | }); 8 | }); 9 | } 10 | function toggle_longlog() { 11 | $(this).parent().children('pre').toggleClass('invisible'); 12 | } 13 | function switch_branch() { 14 | var branch = $(this).attr('value'); 15 | if($.inArray(action, ['commits', 'commit'])>=0) { 16 | url = '/' + repo + '/' + action + '/' + branch + '/' 17 | } 18 | else if($.inArray(action, ['blob','tree'])>=0) { 19 | url = '/' + repo + '/' + action + '/' + branch + '/' + (path ? path : ''); 20 | } 21 | else if(action == 'repo') { 22 | url = '/' + repo + '/tree/' + branch + '/' 23 | } 24 | window.location = url; 25 | } 26 | function init_clone_urls() { 27 | $('.urllink').each(function(index, elt) { 28 | $(elt).click(function() { 29 | $('#cloneurl').attr('value', $(this).children('span').html()); 30 | }); 31 | }); 32 | $('#cloneurl').attr('value', $('.urllink').first().children('span').html()); 33 | } 34 | function add_plain_link() { 35 | $('.actions').prepend('plain | ') 36 | } 37 | 38 | now = new Date().getTime() / 1000; 39 | function humantime(ctime) { 40 | timediff = now - ctime; 41 | if(timediff < 0) 42 | return 'in the future'; 43 | if(timediff < 60) 44 | return 'just now'; 45 | if(timediff < 120) 46 | return 'a minute ago'; 47 | if(timediff < 3600) 48 | return Math.floor(timediff / 60) + " minutes ago" 49 | if(timediff < 7200) 50 | return "an hour ago"; 51 | if(timediff < 86400) 52 | return Math.floor(timediff / 3600) + " hours ago" 53 | if(timediff < 172800) 54 | return "a day ago"; 55 | if(timediff < 2592000) 56 | return Math.floor(timediff / 86400) + " days ago" 57 | if(timediff < 5184000) 58 | return "a month ago"; 59 | if(timediff < 31104000) 60 | return Math.floor(timediff / 2592000) + " months ago" 61 | if(timediff < 62208000) 62 | return "a year ago"; 63 | return Math.floor(timediff / 31104000) + " years ago" 64 | } 65 | -------------------------------------------------------------------------------- /goblet/themes/default/static/link_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/link_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/logo.png -------------------------------------------------------------------------------- /goblet/themes/default/static/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #60a0b0; font-style: italic } /* Comment */ 3 | .err { border: 1px solid #FF0000 } /* Error */ 4 | .k { color: #007020; font-weight: bold } /* Keyword */ 5 | .o { color: #666666 } /* Operator */ 6 | .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #007020 } /* Comment.Preproc */ 8 | .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ 9 | .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #808080 } /* Generic.Output */ 16 | .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #007020 } /* Keyword.Pseudo */ 24 | .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #902000 } /* Keyword.Type */ 26 | .m { color: #40a070 } /* Literal.Number */ 27 | .s { color: #4070a0 } /* Literal.String */ 28 | .na { color: #4070a0 } /* Name.Attribute */ 29 | .nb { color: #007020 } /* Name.Builtin */ 30 | .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 31 | .no { color: #60add5 } /* Name.Constant */ 32 | .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #007020 } /* Name.Exception */ 35 | .nf { color: #06287e } /* Name.Function */ 36 | .nl { color: #002070; font-weight: bold } /* Name.Label */ 37 | .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #062873; font-weight: bold } /* Name.Tag */ 39 | .nv { color: #bb60d5 } /* Name.Variable */ 40 | .ow { color: #007020; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #40a070 } /* Literal.Number.Float */ 43 | .mh { color: #40a070 } /* Literal.Number.Hex */ 44 | .mi { color: #40a070 } /* Literal.Number.Integer */ 45 | .mo { color: #40a070 } /* Literal.Number.Oct */ 46 | .sb { color: #4070a0 } /* Literal.String.Backtick */ 47 | .sc { color: #4070a0 } /* Literal.String.Char */ 48 | .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #4070a0 } /* Literal.String.Double */ 50 | .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 51 | .sh { color: #4070a0 } /* Literal.String.Heredoc */ 52 | .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 53 | .sx { color: #c65d09 } /* Literal.String.Other */ 54 | .sr { color: #235388 } /* Literal.String.Regex */ 55 | .s1 { color: #4070a0 } /* Literal.String.Single */ 56 | .ss { color: #517918 } /* Literal.String.Symbol */ 57 | .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #bb60d5 } /* Name.Variable.Class */ 59 | .vg { color: #bb60d5 } /* Name.Variable.Global */ 60 | .vi { color: #bb60d5 } /* Name.Variable.Instance */ 61 | .il { color: #40a070 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /goblet/themes/default/static/repo_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/repo_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/static/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /goblet/themes/default/static/script_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/script_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/static/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/search.png -------------------------------------------------------------------------------- /goblet/themes/default/static/up_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveas/goblet/fa92dd93f4df708d0ed22b75718da818e2218fe6/goblet/themes/default/static/up_icon.png -------------------------------------------------------------------------------- /goblet/themes/default/templates/base.html: -------------------------------------------------------------------------------- 1 | {# 2 | Goblet - Web based git repository browser 3 | Copyright (C) 2012-2014 Dennis Kaarsemaker 4 | See the LICENSE file for licensing details 5 | #} 6 | 7 | 8 |
Parent commit{% if commit.parents|dlength > 1%}s{% endif %} | 26 |
27 | {% for parent in commit.parents %}
28 | {{ parent.hex[:7] }} {{ parent.message|shortmsg }} 29 | {% endfor %} 30 | |
31 |
{{ decode(file.new_file_path) }} | 51 | {% if file.hunks %} 52 |53 | {% if stat[file.new_file_path]['+'] %}+{{ stat[file.new_file_path]['+'] }}{% endif %}{% if stat[file.new_file_path]['-'] %}{% if stat[file.new_file_path]['+'] %}/{% endif %}-{{ stat[file.new_file_path]['-'] }}{% endif %} 54 | | 55 |56 | | 58 | {% else %} 59 | 57 |(Binary file) | 60 | {% endif %} 61 |
---|
Binary file change | ||
… | … | @@ -{{old}},{{hunk.old_lines}} + {{new}},{{hunk.new_lines}} |
{{ old }} | {{ new }} | {{ decode(line) }} |
{{ old }} | {{ decode(line) }} | |
{{ new }} | {{ decode(line) }} |
Parent commit{% if commit.parents|length > 1%}s{% endif %} | 27 |
28 | {% for parent in commit.parents %}
29 | {{ parent.hex[:7] }} {{ parent.message|shortmsg }} 30 | {% endfor %} 31 | |
32 |
{{ lineno }} | {{ decode(line)|highlight(request.args['q']) }} |
… | 30 | {% endfor %} 31 | |
Name | Last change | ||
---|---|---|---|
![]() |
18 | (up one folder) | 19 |20 | | 21 | |
28 | {% if link %}{% endif %} 29 | {{ decode(file.name) }}{% if S_ISGITLNK(file.filemode) %}@{{ file.hex[:7] }}{% endif %} 30 | {% if link %}{% endif %} 31 | | 32 | {% endwith %} 33 |34 | | 35 | {% if loop.index0 == 0 %} 36 | Loading commit data... 37 | {% else %} 38 | 39 | {% endif %} 40 | | 41 |