├── .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 = """

About git & goblet

38 |

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 \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Goblet.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Goblet.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Goblet" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Goblet" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Goblet documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Mar 17 11:33:58 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import cloud_sptheme as csp 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = [] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Goblet' 45 | copyright = u'2012-2014, Dennis Kaarsemaker' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.3' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.3.5' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'cloud' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | html_theme_options = { 101 | 'roottarget': 'index', 102 | 'stickysidebar': False, 103 | } 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | html_theme_path = [csp.get_theme_dir()] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | html_show_sourcelink = False 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'Gobletdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'Goblet.tex', u'Goblet Documentation', 191 | u'Dennis Kaarsemaker', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'goblet', u'Goblet Documentation', 221 | [u'Dennis Kaarsemaker'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'Goblet', u'Goblet Documentation', 235 | u'Dennis Kaarsemaker', 'Goblet', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | -------------------------------------------------------------------------------- /docs/configuring.rst: -------------------------------------------------------------------------------- 1 | .. Goblet - Web based git repository browser 2 | Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | See the LICENSE file for licensing details 4 | Configuring goblet 5 | ================== 6 | Goblet can be run without any configuration for a quick test, using the 7 | built-in webserver. This is not suitable for any production environment, but is 8 | very helpful to see if all parts work. Change directory to a directory that 9 | contains one or more git repositories and start goblet:: 10 | 11 | cd /home/dennis/code 12 | GOBLET_DEBUG=1 python -mgoblet 13 | 14 | This places werkzeug (the underlying wsgi library) in debug mode. Never do this 15 | in production, as it allows people to execute arbitrary code on your server. 16 | 17 | Goblet configuration 18 | -------------------- 19 | Goblet itself takes only a few configuration variables to alter its behaviour. 20 | Most important are the root directory for all repositories and the logging 21 | settings. The configuration is actually python code, so you can do interesting 22 | tricks with it. The goblet tarball ships an example configuration with all 23 | parameters and documentation for all of them, please refer to it when creating 24 | your own configuration. 25 | 26 | uwsgi configuration 27 | ------------------- 28 | The prefered way to serve flask applications like goblet, is to use uwsgi and a 29 | webserver that speaks the wsgi protocol. A document example config is shipped 30 | with goblet. You will need to modify it to your reality (filesystem paths) and 31 | then you can run it. Do *not* run uwsgi as root, but create a special user 32 | account to run uwsgi or use the same user as for your webserer (www-data on 33 | Debian/Ubuntu) and configure uwsgi to switch to that user. 34 | 35 | Once configured, it can be started with:: 36 | 37 | sudo uwsgi --ini /path/to/uwsgi.ini 38 | 39 | And killed again with:: 40 | 41 | sudo uwsgi --stop /run/uwsgi.pid 42 | 43 | Webserver configuration 44 | ----------------------- 45 | I use nginx to serve goblet, and the example config shipped with goblet is the 46 | same as the one I use, except for some filesystem paths and the hostname. The 47 | configuration integrates goblet, git's http-backend for serving the actual 48 | repositories, and lets you serve files in your repository root as well. If you 49 | make goblet work using another httpd, please share your configuration. 50 | 51 | fcgiwrap 52 | -------- 53 | To make nginx execute the git smart http backend, you will also need to install 54 | and run fcgiwrap. Make sure you edit its initscript and add a line that says:: 55 | 56 | export HOME=/nonexistent 57 | 58 | If you do not do this, git will try to read :file:`/root/.gitconfig`, which it 59 | cannot do. 60 | 61 | Repository configuration 62 | ------------------------ 63 | 64 | Per-repository configuration is not needed either, but like gitweb, goblet can 65 | read some information from the git configuration. 66 | 67 | * A description for the repository is read from the :file:`.git/description` 68 | file. 69 | * The owner of the repository is determined from filesystem permissions, but 70 | can be overridden by setting the goblet.owner variable in a repository. 71 | * You can tell your visitors where to clone your repositories from. The default 72 | is to determine it from the :data:`CLONE_URLS_BASE` variable in goblet.conf, 73 | but can be overridden per repository by setting goblet.clone_url_ssh (and 74 | _git, and _http) 75 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Goblet - Web based git repository browser 2 | Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | See the LICENSE file for licensing details 4 | Serving git repositories with goblet 5 | ==================================== 6 | 7 | Goblet is a fast, easy to customize web frontend for `git`_ repositories. It 8 | was created because existing alternatives are either not very easy to 9 | customize (gitweb), require C programming to do so (cgit), or are tied into 10 | other products, such as bugtrackers (redmin, github). 11 | 12 | Goblet is currently in alpha status, so not all goals have been met yet. 13 | Contributions are welcome, the most useful contribution is using it and 14 | reporting all issues you have. 15 | 16 | If you want to see goblet in action, you can find a running instance on 17 | `kaarsemaker.net`_. If you like what you see, proceed to :doc:`install` and 18 | enjoy! 19 | 20 | Should you hit a problem installing or using goblet, please report it on 21 | `github`_. Reports about uncaught python exceptions should include full 22 | backtraces. If the repository triggering the bug/issue is public, please 23 | include a link to the repository and the link to the bug so I can reproduce it. 24 | 25 | Features 26 | -------- 27 | The following features have been implemented already. 28 | 29 | * Basic repository browsing, files and commits 30 | * Integration with git-http-backend for serving repositories 31 | * Syntax highlighting of source code 32 | * Snapshot downloads for all commits and tags 33 | * Blame output like gitweb 34 | * Directory lists like github, including last change per file 35 | 36 | Features that are planned, but not implemented include: 37 | 38 | * Caching of generated html (snapshots and blame-tree output are cached) 39 | * Extensibility, including better integration of documentation 40 | * Theming 41 | 42 | Goblet is a repository *browser*. Any patch or extension that enhances browsing 43 | functionality is welcome, but things like a bugtracker or repository management 44 | is not what goblet will grow. For both of these, good existing alternatives 45 | exist. Patches that improve the cooperation between goblet and e.g. gitolite 46 | would be welcome, but reimplementing gitolite in goblet would be the wrong 47 | thing to do. 48 | 49 | Table of contents 50 | ----------------- 51 | .. toctree:: 52 | :maxdepth: 2 53 | 54 | install 55 | configuring 56 | 57 | .. _`git`: http://git-scm.com 58 | .. _`github`: https://github.com/seveas/goblet/issues 59 | .. _`kaarsemaker.net`: http://git.kaarsemaker.net 60 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. Goblet - Web based git repository browser 2 | Copyright (C) 2012-2014 Dennis Kaarsemaker 3 | See the LICENSE file for licensing details 4 | Installing goblet 5 | ================= 6 | Goblet is easiest to install on Ubuntu 12.10, as I provide packages for it 7 | myself. Most dependencies are also easy to install, but goblet uses 8 | libgit2/pygit2, and a patched git to provide its features. These projects are 9 | all still quite unstable. The packages I provide work, but if you want to 10 | compile them yourself, please follow the instructions below. 11 | 12 | To use the packages I provide, add my personal package archives to your ubuntu 13 | system with the following commands:: 14 | 15 | sudo add-apt-repository ppa:dennis/devtools 16 | sudo add-apt-repository ppa:dennis/python 17 | sudo apt-get update 18 | 19 | Now you can install goblet and all its dependencies with a single apt command:: 20 | 21 | sudo apt-get install goblet 22 | 23 | Non-python dependencies 24 | ----------------------- 25 | The only non-python dependencies are xz, git and groff. Goblet does require a 26 | patched git though. If you do not use the packages I provide, please build 27 | your own git with Jeff King's blame-tree patches applied. You can find 28 | these, rebased against the latest git version, as the last four patches on `my 29 | git clone`_ 30 | 31 | Python dependencies 32 | ------------------- 33 | Goblet requires Python 2.6 or newer, python 3 is not yet supported. It has 34 | only a few python dependencies: the flask/werkzeug/jinja2/pygments 35 | combination as web framework, markdown and docutils for rendering markdown and 36 | rst, and whelk for executing git commands. These can all be installed with 37 | pip, or from my repositories or the Ubuntu repositories. 38 | 39 | pygit2 40 | ------ 41 | As the libgit2 and pygit2 projects do not yet provide stable releases, they 42 | need to be built from a git checkout if you do not use the packages I provide. 43 | As no API compatibility is guaranteed, it is best to use the exact same 44 | versions as me. The following sequence of commands will download, build and 45 | install libgit2 and pygit2 into :file:`/usr/local`:: 46 | 47 | sudo apt-get install cmake python-all-devel 48 | 49 | git clone git://github.com/libgit2/libgit2.git 50 | pushd libgit2 51 | git checkout bb19532 52 | mkdir build && cd build 53 | cmake --build .. 54 | sudo cmake --build . --target install 55 | popd 56 | 57 | git clone git://github.com/libgit2/pygit2.git 58 | pushd pygit2 59 | git checkout 29ce23c 60 | python setup.py build 61 | sudo python setup.py install 62 | popd 63 | 64 | sudo ldconfig 65 | 66 | Goblet itself 67 | ------------- 68 | When all dependencies have been installed, goblet can be installed with pip, or 69 | you can live on the bleeding edge and clone the git repository from github:: 70 | 71 | git clone git://github.com/seveas/goblet.git 72 | cd goblet 73 | git submodule init 74 | git submodule update 75 | 76 | With git now installed, please proceed to :doc:`configuring` and learn how to 77 | configure goblet. 78 | 79 | .. _`my git clone`: https://github.com/seveas/git/commits/dk/private 80 | -------------------------------------------------------------------------------- /goblet/__init__.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.__main__ import app 6 | -------------------------------------------------------------------------------- /goblet/__main__.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 | # If we're running from a git checkout, make sure we use the checkout 6 | import os, sys 7 | git_checkout = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | git_checkout = os.path.exists(os.path.join(git_checkout, '.git')) and git_checkout or None 9 | if git_checkout: 10 | sys.path.insert(0, git_checkout) 11 | 12 | from flask import Flask 13 | import goblet.monkey 14 | import goblet.filters 15 | import goblet.views as v 16 | import goblet.json_views as j 17 | from goblet.encoding import decode 18 | import stat 19 | 20 | class Defaults: 21 | REPO_ROOT = git_checkout and os.path.dirname(git_checkout) or os.getcwd() 22 | DAVATAR_SERVER = None 23 | DAVATAR_SERVER = 'http://davatar.seveas.net/avatar' 24 | MAX_SEARCH_DEPTH = 2 25 | CACHE_ROOT = '/tmp/goblet-snapshots' 26 | USE_X_SENDFILE = False 27 | USE_X_ACCEL_REDIRECT = False 28 | ADMINS = [] 29 | SENDER = 'webmaster@localhost' 30 | CLONE_URLS_BASE = {} 31 | DEBUG = os.environ.get('GOBLET_DEBUG', 'False').lower() == 'true' 32 | THEME = 'default' 33 | ABOUT = """

About git & goblet

34 |

35 | 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('//', view_func=v.RepoView.as_view('repo')) 83 | app.add_url_rule('//tree//', view_func=v.TreeView.as_view('tree')) 84 | app.add_url_rule('/j//treechanged//', view_func=j.TreeChangedView.as_view('treechanged')) 85 | app.add_url_rule('//history/', view_func=v.HistoryView.as_view('history')) 86 | app.add_url_rule('//blame/', view_func=v.BlobView.as_view('blame')) 87 | app.add_url_rule('//blob/', view_func=v.BlobView.as_view('blob')) 88 | app.add_url_rule('//raw/', view_func=v.RawView.as_view('raw')) 89 | app.add_url_rule('//patch//', view_func=v.PatchView.as_view('patch')) 90 | app.add_url_rule('//commit//', view_func=v.CommitView.as_view('commit')) 91 | app.add_url_rule('//commits/', view_func=v.LogView.as_view('commits')) 92 | app.add_url_rule('//commits//', view_func=v.LogView.as_view('ref_commits')) 93 | app.add_url_rule('//tags/', view_func=v.TagsView.as_view('tags')) 94 | app.add_url_rule('//snapshot///', view_func=v.SnapshotView.as_view('snapshot')) 95 | 96 | # Logging 97 | if not app.debug and app.config['ADMINS']: 98 | import logging, logging.handlers 99 | class SMTPHandler(logging.handlers.SMTPHandler): 100 | def format(self, msg): 101 | from flask import request 102 | msg = super(SMTPHandler, self).format(msg) 103 | get = '\n'.join(['%s=%s' % (x, request.args[x]) for x in request.args]) 104 | post = '\n'.join(['%s=%s' % (x, str(request.form[x])[:1000]) for x in request.form]) 105 | cookies = '\n'.join(['%s=%s' % (x, request.cookies[x]) for x in request.cookies]) 106 | env = '\n'.join(['%s=%s' % (x, request.environ[x]) for x in request.environ]) 107 | hdr = '\n'.join([': '.join(x) for x in request.headers]) 108 | return ("%s\n\nGET variables:\n%s\n\nPOST variables:\n%s\n\nCookies:\n%s\n\n" + 109 | "HTTP Headers:\n%s\n\nEnvironment:\n%s") % (msg, get, post, cookies, hdr, env) 110 | 111 | mail_handler = SMTPHandler('127.0.0.1', app.config['SENDER'], app.config['ADMINS'], "Goblet error") 112 | mail_handler.setLevel(logging.ERROR) 113 | app.logger.addHandler(mail_handler) 114 | 115 | if __name__ == '__main__': 116 | os.chdir('/') 117 | app.run() 118 | -------------------------------------------------------------------------------- /goblet/encoding.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 chardet 6 | 7 | def decode(data, encoding=None): 8 | if isinstance(data, unicode): 9 | return data 10 | if encoding: 11 | return data.decode(encoding) 12 | try: 13 | return data.decode('utf-8') 14 | except UnicodeDecodeError: 15 | encoding = chardet.detect(data)['encoding'] 16 | if not encoding: 17 | return "(Binary data)" 18 | return data.decode(encoding) 19 | -------------------------------------------------------------------------------- /goblet/filters.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 current_app as app, url_for 6 | from jinja2 import Markup, escape, Undefined 7 | from collections import defaultdict 8 | import hashlib 9 | from goblet.memoize import memoize 10 | from goblet.encoding import decode as decode_ 11 | import stat 12 | import time 13 | import re 14 | import urllib 15 | 16 | filters = {} 17 | def filter(name_or_func): 18 | if callable(name_or_func): 19 | filters[name_or_func.__name__] = name_or_func 20 | return name_or_func 21 | def decorator(func): 22 | filters[name_or_func] = func 23 | return func 24 | return decorator 25 | 26 | @filter('gravatar') 27 | @memoize 28 | def gravatar(email, size=21): 29 | default = 'mm' 30 | if app.config['DAVATAR_SERVER'] and '@' in email: 31 | default = urllib.quote('%s/%s/%d/%s/davatar.jpg' % (app.config['DAVATAR_SERVER'], email[email.find('@')+1:], size, default)) 32 | return 'http://www.gravatar.com/avatar/%s?s=%d&d=%s' % (hashlib.md5(email).hexdigest(), size, default) 33 | 34 | @filter 35 | def humantime(ctime): 36 | timediff = time.time() - ctime 37 | if timediff < 0: 38 | return 'in the future' 39 | if timediff < 60: 40 | return 'just now' 41 | if timediff < 120: 42 | return 'a minute ago' 43 | if timediff < 3600: 44 | return "%d minutes ago" % (timediff / 60) 45 | if timediff < 7200: 46 | return "an hour ago" 47 | if timediff < 86400: 48 | return "%d hours ago" % (timediff / 3600) 49 | if timediff < 172800: 50 | return "a day ago" 51 | if timediff < 2592000: 52 | return "%d days ago" % (timediff / 86400) 53 | if timediff < 5184000: 54 | return "a month ago" 55 | if timediff < 31104000: 56 | return "%d months ago" % (timediff / 2592000) 57 | if timediff < 62208000: 58 | return "a year ago" 59 | return "%d years ago" % (timediff / 31104000) 60 | 61 | @filter 62 | def shortmsg(message): 63 | message += "\n" 64 | short, long = message.split('\n', 1) 65 | if len(short) > 80: 66 | short = escape(short[:short.rfind(' ',0,80)]) + Markup('…') 67 | return short 68 | 69 | @filter 70 | def longmsg(message): 71 | message += "\n" 72 | short, long = message.split('\n', 1) 73 | if len(short) > 80: 74 | long = message 75 | long = re.sub(r'^[-a-z]+(-[a-z]+)*:.+\n', '', long, flags=re.MULTILINE|re.I).strip() 76 | if not long: 77 | return "" 78 | return Markup('') % 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("") % url_for('raw', repo=repo.name, path="/".join([ref, path])) 96 | 97 | @renderer 98 | def plain(repo, ref, path, entry): 99 | data = escape(decode(repo[entry.oid].data)) 100 | data = re.sub(r'(https?://(?:[-a-zA-Z0-9\._~:/?#\[\]@!\'()*+,;=]+|&)+)', Markup(r'\1'), data) 101 | return Markup(u"
%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 | {% block title %}Git{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 |
30 | Git 31 | 34 |
35 |
36 | {% block content %} 37 | {% endblock %} 38 |
39 | 42 | 43 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/blob.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}{{ path }}{% endblock %} 8 | {% block repo_content %} 9 |

{{ repo.name }} / {% if folder %}{{ folder }}/{{ decode(file.name) }}{% else %}{{ path }}{% endif %}

10 |
11 |

12 | {{ decode(file.name) }} 13 | 14 | {% if request.endpoint != 'blob' %}blob | {% endif %} 15 | raw | 16 | {% if request.endpoint != 'blame' %}blame | {% endif %} 17 | history 18 | 19 |

20 |
21 | {{ rendered_file }} 22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/commit.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}{{ commit.message|shortmsg }} · {{ commit.hex[:7] }}{% endblock %} 8 | {% block repo_content %} 9 |
{{ commit.commit_time|strftime("%b %d, %Y") }} 10 | 11 | Download as patch, 12 | tar.xz or 13 | zip file. 14 | 15 |
16 |
17 |
18 | 19 | {{ commit.message|shortmsg }} 20 | {{ commit.message|longmsg }} 21 |
22 | {% if commit.parents %} 23 | 24 | 25 | 26 | 31 |
Parent commit{% if commit.parents|dlength > 1%}s{% endif %} 27 | {% for parent in commit.parents %} 28 | {{ parent.hex[:7] }} {{ parent.message|shortmsg }}
29 | {% endfor %} 30 |
32 | {% endif %} 33 | Created by {{ commit.author.name }}, {{ commit.author.time|humantime }} 34 | {% if commit.committer.email != commit.author.email %} 35 |
Committed by {{ commit.committer.name }}, {{ commit.committer.time|humantime }} 36 | {% for ack, who in commit.message|acks %}
{{ ack }}: {{ who|join(", ") }}{% endfor %} 37 | {% endif %} 38 |
39 |
40 |
41 |
42 |

43 | Showing {{ diff|dlength }} changed file{% if diff|dlength != 1 %}s{% endif %} 44 | ({{ stat[None]['+'] }} addition{% if stat[None]['+'] != 1 %}s{% endif %}, 45 | {{ stat[None]['-'] }} deletion{% if stat[None]['-'] != 1 %}s{% endif %}) 46 |

47 | 48 | {% for file in diff %} 49 | 50 | 51 | {% if file.hunks %} 52 | 55 | 58 | {% else %} 59 | 60 | {% endif %} 61 | 62 | {% endfor %} 63 |
{{ decode(file.new_file_path) }} 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 | 56 |
 
57 |
(Binary file)
64 | {% for file in diff %} 65 |
66 | {% set file_link = tree_link(repo, commit.hex, path, file.new_file_path) %} 67 |

{{ decode(file.new_file_path) }} 68 | 69 | {% if commit.parents %} 70 | {% set old_file_link = tree_link(repo, commit.parents[0].hex, path, file.old_file_path) %} 71 | {% endif %} 72 | view | 73 | raw | 74 | blame | 75 | history 76 | 77 |

78 | 79 | {% if not file.hunks %} 80 | 81 | {% endif %} 82 | {% for hunk in file.hunks %} 83 | {% set old = hunk.old_start %} 84 | {% set new = hunk.new_start %} 85 | 86 | {% for status,line in hunk.lines %} 87 | {% if status == ' ' %} 88 | 89 | {% set old = old + 1 %}{% set new = new +1 %} 90 | {% elif status == '-' %} 91 | 92 | {% set old = old + 1 %} 93 | {% elif status == '+' %} 94 | 95 | {% set new = new + 1 %} 96 | {% endif %} 97 | {% endfor %} 98 | {% endfor %} 99 |
  
Binary file change
@@ -{{old}},{{hunk.old_lines}} + {{new}},{{hunk.new_lines}}
{{ old }}{{ new }}
{{ decode(line) }}
{{ old }} 
{{ decode(line) }}
 {{ new }}
{{ decode(line) }}
100 |
101 | 102 | {% endfor %} 103 |
104 | {% endblock %} 105 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/log.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}Commits{% endblock %} 8 | {% block repo_content %} 9 |
10 | {% for commit in log %} 11 | {% if last_date != commit.commit_time - commit.commit_time % 86400 %} 12 | {% set last_date = commit.commit_time - commit.commit_time % 86400 %} 13 |
{{ commit.commit_time|strftime("%b %d, %Y") }}
14 | {% endif %} 15 | 16 |
17 |
18 | 19 | {{ commit.message|shortmsg }} {% if commit.message|longmsg %}more…{% endif %} 20 | {% for reftype, refid in refs[commit.hex] %}{{ refid }}{% endfor %} 21 | {{ commit.message|longmsg }} 22 |
23 | {% if commit.parents %} 24 | 25 | 26 | 27 | 32 |
Parent commit{% if commit.parents|length > 1%}s{% endif %} 28 | {% for parent in commit.parents %} 29 | {{ parent.hex[:7] }} {{ parent.message|shortmsg }}
30 | {% endfor %} 31 |
33 | {% endif %} 34 | Created by {{ commit.author.name }}, {{ commit.author.time|humantime }} 35 | {% if commit.committer.email != commit.author.email %} 36 |
Committed by {{ commit.committer.name }}, {{ commit.committer.time|humantime }} 37 | {% for ack, who in commit.message|acks %}
{{ ack }}: {{ who|join(", ") }}{% endfor %} 38 | {% endif %} 39 |
40 |
41 |
42 | {% endfor %} 43 |
44 | 45 | {% if pref_page or next_page %} 46 | 58 | {% endif %} 59 | 60 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/nocommits.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 | {% extends "base.html" %} 7 | {% block content %} 8 |

{{ repo.name }} by {{ repo.owner }}

9 | {{ repo.description }} 10 |
11 | There are no commits yet in this repository 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/repo_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 | {% extends "base.html" %} 7 | {% block title %}{{ repo.name }} · {% block subtitle %}{% endblock %}{% endblock %} 8 | {% block content %} 9 |

{{ repo.name }} by {{ repo.owner }}

10 | {{ repo.description }} {% if readme %}[read more]{% endif %} 11 | {% if show_clone_urls and repo.clone_urls %} 12 | 19 | 22 | {% endif %} 23 |
24 |
    25 | {% if commit %} 26 |
  • Files
  • 27 | {% else %} 28 |
  • Files
  • 29 | {% endif %} 30 | {% if ref %} 31 |
  • Commits
  • 32 | {% else %} 33 |
  • Commits
  • 34 | {% endif %} 35 |
  • Tags
  • 36 |
  • 44 |
45 |
46 | {% block repo_content %} 47 | {% endblock %} 48 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/repo_index.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 | {% extends "base.html" %} 7 | {% block title %}Git repositories on {{ request.host }}{% endblock %} 8 | {% block content %} 9 | {% if config['ABOUT'] %} 10 |
11 | {{ config['ABOUT']|safe }} 12 |
13 | {% endif %} 14 |

Git repositories on {{ request.host }}

15 | 16 | {% for repo in repos %} 17 |
18 |

{{ repo.name }} by {{ repo.owner }}

19 | {{ repo.description }} 20 |
21 | {% if repo.head %} 22 | Last updated {{ repo[repo.head.target].commit_time|humantime }}, 23 | by {{ repo[repo.head.target].author.name }} 24 | {% else %} 25 | No commits yet 26 | {% endif %} 27 |
28 |
29 | {% endfor %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/search.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}Search{% endblock %} 8 | {% block repo_content %} 9 | {% if total == 0 %} 10 |

No search results for {{ request.args['q'] }}

11 | {% else %} 12 |

Results {{ start }}..{{ end }} of {{ total }} for {{ request.args['q'] }}

13 | {% for file, chunks in results %} 14 |
15 |

{{ file[1] }} 16 | 17 | {% set file_link = tree_link(repo, ref, path, file[1]) %} 18 | view | 19 | raw | 20 | blame | 21 | history 22 | 23 |

24 | 25 | {% for chunk in chunks %} 26 | {% for lineno, line in chunk %} 27 | 28 | {% endfor %} 29 | 30 | {% endfor %} 31 |
{{ lineno }}
{{ decode(line)|highlight(request.args['q']) }}
 
32 |
33 | {% endfor %} 34 | 35 | {% if prev_page or next_page %} 36 | 48 | {% endif %} 49 | {% endif %} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/tags.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}Tags & snapshots{% endblock %} 8 | {% block repo_content %} 9 |
10 |
Tag {{ start }}…{{ end }} of {{ total }}
11 | {% for name, tag, commit in tags %} 12 |
13 |
14 | {% if tag.tagger %} 15 | 16 | {% endif %} 17 | {{ name }}{% if tag %} - {{ tag.message|shortmsg }}{% endif %}
18 | Download as tar.xz or 19 | zip file. 20 |
21 | {% if tag.tagger %} 22 | {{ tag.tagger.name }} tagged {{ tag.tagger.time|humantime }}
23 | {% endif %} 24 | Commit {{ commit.hex[:7] }} - {{ commit.message|shortmsg }} 25 |
26 |
27 |
28 | {% endfor %} 29 |
30 | 31 | {% if prev_page or next_page %} 32 | 44 | {% endif %} 45 | 46 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /goblet/themes/default/templates/tree.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 | {% extends "repo_base.html" %} 7 | {% block subtitle %}Files{% endblock %} 8 | {% block repo_content %} 9 |

{{ repo.name }} / {{ path }}

10 | 11 | 12 | 13 | 14 | 15 | {% if path != "" %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endif %} 23 | {% for file in tree %} 24 | 25 | 26 | {% with link = tree_link(repo, ref, path, file) %} 27 | 32 | {% endwith %} 33 | 34 | 41 | 42 | {% endfor %} 43 |
 NameLast changeMessage
(up one folder)                               
28 | {% if link %}{% endif %} 29 | {{ decode(file.name) }}{% if S_ISGITLNK(file.filemode) %}@{{ file.hex[:7] }}{% endif %} 30 | {% if link %}{% endif %} 31 |                                35 | {% if loop.index0 == 0 %} 36 | Loading commit data... 37 | {% else %} 38 |   39 | {% endif %} 40 |
44 | {% if readme %} 45 |
46 | 47 |

{{ readme.name }}

48 |
49 | {{ rendered_file }} 50 |
51 |
52 | {% endif %} 53 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /goblet/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 flask import render_template, current_app, redirect, url_for, request, send_file 6 | from flask.views import View 7 | from goblet.encoding import decode 8 | from goblet.render import render 9 | import os 10 | import glob 11 | import pygit2 12 | import re 13 | import stat 14 | import chardet 15 | import mimetypes 16 | from collections import namedtuple 17 | from whelk import shell 18 | 19 | class NotFound(Exception): 20 | pass 21 | 22 | class TemplateView(View): 23 | def render(self, context): 24 | return render_template(self.template_name, **context) 25 | def nocommits(self, context): 26 | return render_template("nocommits.html", **context) 27 | 28 | class IndexView(TemplateView): 29 | template_name = 'repo_index.html' 30 | 31 | def list_repos(self, root, level): 32 | for file in os.listdir(root): 33 | path = os.path.join(root, file) 34 | if not os.path.isdir(path) or not os.access(path, os.R_OK): 35 | continue 36 | if path.endswith('.git'): 37 | yield path 38 | elif os.path.exists(os.path.join(path, '.git')): 39 | yield os.path.join(path, '.git') 40 | elif level > 0: 41 | for path in self.list_repos(path, level-1): 42 | yield path 43 | 44 | def dispatch_request(self): 45 | root = current_app.config['REPO_ROOT'] 46 | depth = current_app.config['MAX_SEARCH_DEPTH'] 47 | repos = [] 48 | for repo in sorted(self.list_repos(root, depth),key=lambda x:x.lower()): 49 | try: 50 | repos.append(pygit2.Repository(repo)) 51 | except KeyError: 52 | # Something was unreadable 53 | pass 54 | for keyword in request.args.get('q', '').lower().split(): 55 | keyword = keyword.strip() 56 | if keyword: 57 | repos = [repo for repo in repos if keyword in repo.name.lower() or keyword in repo.description.lower()] 58 | return self.render({'repos': repos}) 59 | 60 | class RepoBaseView(TemplateView): 61 | template_name = None 62 | def dispatch_request(self, repo, *args, **kwargs): 63 | root = current_app.config['REPO_ROOT'] 64 | try: 65 | repo = pygit2.Repository(os.path.join(root, repo)) 66 | except KeyError: 67 | return "No such repo", 404 68 | if not repo.head: 69 | return self.nocommits({'repo': repo}) 70 | data = {'repo': repo, 'action': request.endpoint} 71 | try: 72 | ret = self.handle_request(repo, *args, **kwargs) 73 | except NotFound, e: 74 | return str(e), 404 75 | if hasattr(ret, 'status_code'): 76 | return ret 77 | 78 | if self.template_name: 79 | data.update(ret) 80 | if 'ref' in data: 81 | data['ref_for_commit'] = repo.ref_for_commit(data['ref']) 82 | elif 'commit' in data: 83 | data['ref_for_commit'] = repo.ref_for_commit(data['commit']) 84 | else: 85 | data['ref_for_commit'] = repo.ref_for_commit(repo.head.target.hex) 86 | return self.render(data) 87 | # For rawview 88 | return ret 89 | 90 | class TagsView(RepoBaseView): 91 | template_name = 'tags.html' 92 | tags_per_page = 50 93 | 94 | def handle_request(self, repo): 95 | tags = [repo.lookup_reference(x) for x in repo.listall_references() if x.startswith('refs/tags')] 96 | tags = [(tag.name[10:], repo[tag.target.hex]) for tag in tags] 97 | # Annotated tags vs normal tags 98 | # Result is list of (name, tag or None, commit) 99 | tags = [(name, hasattr(tag, 'target') and tag or None, hasattr(tag, 'target') and repo[tag.target] or tag) for name, tag in tags] 100 | # Filter out non-commits 101 | tags = [x for x in tags if x[2].type == pygit2.GIT_OBJ_COMMIT] 102 | # Sort by tag-time or commit-time 103 | tags.sort(reverse=True, key=lambda t: t[1] and t[1].tagger and t[1].tagger.time or t[2].commit_time) 104 | for keyword in request.args.get('q', '').lower().split(): 105 | keyword = keyword.strip() 106 | if keyword: 107 | tags = [tag for tag in tags if keyword in tag[0] or 108 | (tag[1] and keyword in tag[1].message) or 109 | ((tag[1] and tag[1].tagger) and (keyword in tag[1].tagger.name or keyword in tag[1].tagger.email))] 110 | 111 | page = 1 112 | try: 113 | page = int(request.args['page']) 114 | except (KeyError, ValueError): 115 | pass 116 | page = max(1,page) 117 | next_page = prev_page = None 118 | total = len(tags) 119 | if page > 1: 120 | prev_page = page - 1 121 | if total > self.tags_per_page * page: 122 | next_page = page + 1 123 | 124 | start = (page-1) * self.tags_per_page 125 | end = min(start + 50, total) 126 | return {'tags': tags[start:end], 'start': start+1, 'end': end, 'total': total, 'prev_page': prev_page, 'next_page': next_page} 127 | 128 | # Repo, path and blob 129 | 130 | class PathView(RepoBaseView): 131 | def split_ref(self, repo, path, expects_file=False): 132 | file = None 133 | # First extract branch, which can contain slashes 134 | for ref in sorted(repo.branches(), key = lambda x: -len(x)): 135 | if path.startswith(ref): 136 | path = path.replace(ref, '')[1:] 137 | tree = repo[repo.lookup_reference('refs/heads/%s' % ref).target.hex].tree 138 | break 139 | else: 140 | # OK, maybe a tag? 141 | for ref in sorted(repo.tags(), key = lambda x: -len(x)): 142 | if path.startswith(ref): 143 | path = path.replace(ref, '')[1:] 144 | ref = repo.lookup_reference('refs/tags/%s' % ref).target.hex 145 | if repo[ref].type == pygit2.GIT_OBJ_TAG: 146 | ref = repo[repo[ref].target].hex 147 | tree = repo[ref].tree 148 | break 149 | else: 150 | # Or a commit 151 | if '/' in path: 152 | ref, path = path.split('/', 1) 153 | else: 154 | ref, path = path, '' 155 | try: 156 | tree = repo[ref].tree 157 | except (KeyError, ValueError): 158 | raise NotFound("No such commit/ref") 159 | 160 | # Remainder is path 161 | path_ = path.split('/') 162 | while path and path_: 163 | if path_[0] not in tree: 164 | raise NotFound("No such file") 165 | entry = tree[path_.pop(0)] 166 | 167 | if expects_file and not path_: 168 | if not stat.S_ISREG(entry.filemode): 169 | raise NotFound("Not a file") 170 | file = entry 171 | elif not stat.S_ISDIR(entry.filemode): 172 | raise NotFound("Not a folder") 173 | else: 174 | tree = repo[entry.oid] 175 | if expects_file and not file: 176 | raise NotFound("No such file") 177 | 178 | return ref, path, tree, file 179 | 180 | class TreeView(PathView): 181 | template_name = 'tree.html' 182 | results_per_page = 50 183 | 184 | def handle_request(self, repo, path): 185 | ref, path, tree, _ = self.split_ref(repo, path) 186 | if 'q' in request.args: 187 | return self.git_grep(repo, ref, path) 188 | return {'tree': tree, 'ref': ref, 'path': path} 189 | 190 | def git_grep(self, repo, ref, path): 191 | self.template_name = 'search.html' 192 | results = list(repo.grep(ref, path, request.args['q'])) 193 | 194 | page = 1 195 | try: 196 | page = int(request.args['page']) 197 | except (KeyError, ValueError): 198 | pass 199 | page = max(1,page) 200 | next_page = prev_page = None 201 | total = len(results) 202 | if page > 1: 203 | prev_page = page - 1 204 | if total > self.results_per_page * page: 205 | next_page = page + 1 206 | 207 | start = (page-1) * self.results_per_page 208 | end = min(start + 50, total) 209 | 210 | return {'results': results[start:end], 'start': start+1, 'end': end, 'total': total, 211 | 'ref': ref, 'path': path, 'next_page': next_page, 'prev_page': prev_page} 212 | 213 | class RepoView(TreeView): 214 | def handle_request(self, repo): 215 | tree = repo[repo.head.target].tree 216 | if 'q' in request.args: 217 | ref = repo.ref_for_commit(repo.head.target.hex) 218 | return redirect(url_for('tree', repo=repo.name, path=ref) + '?q=' + request.args['q']) 219 | readme = renderer = rendered_file = None 220 | for file in tree: 221 | if re.match(r'^readme(?:.(?:txt|rst|md))?$', file.name, flags=re.I): 222 | readme = file 223 | renderer, rendered_file = render(repo, repo.head, '', readme) 224 | return {'readme': readme, 'tree': tree, 'ref': repo.ref_for_commit(repo.head.target.hex), 225 | 'path': '', 'show_clone_urls': True, 'renderer': renderer, 'rendered_file': rendered_file} 226 | 227 | class BlobView(PathView): 228 | template_name = 'blob.html' 229 | 230 | def handle_request(self, repo, path): 231 | ref, path, tree, file = self.split_ref(repo, path, expects_file=True) 232 | folder = '/' in path and path[:path.rfind('/')] or None 233 | renderer, rendered_file = render(repo, ref, path, file, blame=request.endpoint == 'blame', plain=request.args.get('plain') == '1') 234 | # For empty blames, a redirect to the history is better 235 | if rendered_file is None: 236 | return redirect(url_for('history', repo=repo.name, path='%s/%s' % (ref, path))) 237 | return {'tree': tree, 'ref': ref, 'path': path, 'file': file, 'folder': folder, 'rendered_file': rendered_file, 'renderer': renderer} 238 | 239 | class RawView(PathView): 240 | template = None 241 | def handle_request(self, repo, path): 242 | ref, path, tree, file = self.split_ref(repo, path, expects_file=True) 243 | if not ref: 244 | raise NotFound("No such file") 245 | 246 | # Try to detect the mimetype 247 | mimetype, encoding = mimetypes.guess_type(file.name) 248 | data = repo[file.hex].data 249 | # shbang'ed: text/plain will do 250 | if not mimetype and data[:2] == '#!': 251 | mimetype = 'text/plain' 252 | # For text mimetypes, guess an encoding 253 | if not mimetype: 254 | if '\0' in data: 255 | mimetype = 'application/octet-stream' 256 | else: 257 | mimetype = 'text/plain' 258 | if mimetype.startswith('text/') and not encoding: 259 | encoding = chardet.detect(data)['encoding'] 260 | headers = {'Content-Type': mimetype} 261 | if encoding: 262 | headers['Content-Encoding'] = encoding 263 | return (data, 200, headers) 264 | 265 | # Log, snapshot, commit and diff 266 | 267 | class RefView(RepoBaseView): 268 | def lookup_ref(self, repo, ref): 269 | if not ref: 270 | return repo.head.target 271 | try: 272 | ref = repo.lookup_reference('refs/heads/' + ref).target.hex 273 | except (KeyError, ValueError): 274 | pass 275 | try: 276 | ref = repo.lookup_reference('refs/tags/' + ref).target.hex 277 | except (KeyError, ValueError): 278 | pass 279 | try: 280 | obj = repo[ref] 281 | if obj.type == pygit2.GIT_OBJ_TAG: 282 | obj = repo[obj.target] 283 | if obj.type != pygit2.GIT_OBJ_COMMIT: 284 | raise NotFound("No such commit/ref") 285 | return obj 286 | except (KeyError, ValueError): 287 | raise NotFound("No such commit/ref") 288 | 289 | class LogView(RefView): 290 | template_name = 'log.html' 291 | commits_per_page = 50 292 | 293 | def handle_request(self, repo, ref=None): 294 | ref = self.lookup_ref(repo, ref) 295 | page = 1 296 | try: 297 | page = int(request.args['page']) 298 | except (KeyError, ValueError): 299 | pass 300 | page = max(1,page) 301 | next_page = prev_page = None 302 | if page > 1: 303 | prev_page = page - 1 304 | 305 | log = list(repo.get_commits(ref, skip=self.commits_per_page * (page-1), count=self.commits_per_page, search=request.args.get('q', ''))) 306 | if log and log[-1].parents: 307 | next_page = page + 1 308 | shas = [x.hex for x in log] 309 | return {'ref': repo.ref_for_commit(ref), 'log': log, 'shas': shas, 'refs': repo.reverse_refs, 'next_page': next_page, 'prev_page': prev_page} 310 | 311 | class HistoryView(PathView,RefView): 312 | template_name = 'log.html' 313 | commits_per_page = 50 314 | 315 | def handle_request(self, repo, path): 316 | ref, path, tree, file = self.split_ref(repo, path, expects_file=True) 317 | ref = self.lookup_ref(repo, ref) 318 | page = 1 319 | try: 320 | page = int(request.args['page']) 321 | except (KeyError, ValueError): 322 | pass 323 | page = max(1,page) 324 | next_page = prev_page = None 325 | if page > 1: 326 | prev_page = page - 1 327 | 328 | log = list(repo.get_commits(ref, skip=self.commits_per_page * (page-1), count=self.commits_per_page, file=path)) 329 | if log and log[-1].parents and len(log) == self.commits_per_page: 330 | next_page = page + 1 331 | shas = [x.hex for x in log] 332 | return {'ref': repo.ref_for_commit(ref), 'log': log, 'shas': shas, 'refs': repo.reverse_refs, 'next_page': next_page, 'prev_page': prev_page} 333 | 334 | 335 | snapshot_formats = { 336 | 'zip': ('zip', None, 'zip' ), 337 | 'xz': ('tar', ['xz'], 'tar.xz' ), 338 | 'gz': ('tar', ['gzip', '-9'], 'tar.gz' ), 339 | 'bz2': ('tar', ['bzip2', '-9'], 'tar.bz2'), 340 | } 341 | class SnapshotView(RefView): 342 | def handle_request(self, repo, ref, format): 343 | ref = self.lookup_ref(repo, ref) 344 | format, compressor, ext = snapshot_formats.get(format, (None, None)) 345 | if not format: 346 | raise NotFound("No such snapshot format") 347 | cache_dir = current_app.config['CACHE_ROOT'] 348 | if not os.path.exists(cache_dir): 349 | os.makedirs(cache_dir) 350 | 351 | desc = repo.describe(ref.hex).replace('/', '-') 352 | filename_compressed = os.path.join(cache_dir, '%s-%s.%s' % (repo.name, desc, ext)) 353 | filename_uncompressed = os.path.join(cache_dir, '%s-%s.%s' % (repo.name, desc, format)) 354 | if not os.path.exists(filename_compressed): 355 | ret = repo.git('archive', '--format', format, '--prefix', '%s-%s/' % (repo.name, desc), '--output', filename_uncompressed, ref.hex) 356 | if ret.returncode != 0: 357 | raise RuntimeError(ret.stderr) 358 | if compressor: 359 | compressor = compressor[:] 360 | compressor.append(filename_uncompressed) 361 | ret = getattr(shell, compressor[0])(*compressor[1:]) 362 | if ret.returncode != 0: 363 | raise RuntimeError(ret.stderr) 364 | return send_file(filename_compressed, attachment_filename=os.path.basename(filename_compressed), as_attachment=True, cache_timeout=86400) 365 | 366 | class CommitView(RefView): 367 | template_name = 'commit.html' 368 | 369 | def handle_request(self, repo, ref=None): 370 | ref = self.lookup_ref(repo, ref) 371 | stat = {} 372 | if not ref.parents: 373 | diff = ref.tree.diff_to_tree(swap=True) 374 | else: 375 | diff = ref.parents[0].tree.diff_to_tree(ref.tree) 376 | for file in diff: 377 | s = stat[file.new_file_path] = {'-': 0, '+': 0} 378 | for hunk in file.hunks: 379 | hs = [x[0] for x in hunk.lines] 380 | s['-'] += hs.count('-') 381 | s['+'] += hs.count('+') 382 | s['%'] = int(100.0 * s['+'] / (s['-']+s['+'])) 383 | stat[None] = {'-': sum([x['-'] for x in stat.values()]), '+': sum([x['+'] for x in stat.values()])} 384 | return {'commit': ref, 'diff': diff, 'stat': stat} 385 | 386 | class PatchView(RefView): 387 | def handle_request(self, repo, ref=None): 388 | ref = self.lookup_ref(repo, ref) 389 | # XXX port to pygit2 390 | data = repo.git('format-patch', '--stdout', '%s^..%s' % (ref.hex, ref.hex)).stdout 391 | return (data, 200, [{'Content-Type': 'text/plain', 'Content-Encoding': 'utf-8'}]) 392 | 393 | Fakefile = namedtuple('Fakefile', ('name', 'filemode')) 394 | def tree_link(repo, ref, path, file): 395 | if isinstance(file, str): 396 | file = Fakefile(name=file, filemode=stat.S_IFREG) 397 | if path: 398 | tree_path = '/'.join([ref, path, decode(file.name)]) 399 | else: 400 | tree_path = '/'.join([ref, decode(file.name)]) 401 | if stat.S_ISDIR(file.filemode): 402 | return url_for('tree', repo=repo.name, path=tree_path) 403 | if stat.S_ISREG(file.filemode): 404 | return url_for('blob', repo=repo.name, path=tree_path) 405 | 406 | def blob_link(repo, ref, path, file=None): 407 | parts = [x for x in (ref, path, file) if x] 408 | return url_for('blob', repo=repo.name, path='/'.join(parts)) 409 | 410 | def raw_link(repo, ref, path, file=None): 411 | parts = [x for x in (ref, path, file) if x] 412 | return url_for('raw', repo=repo.name, path='/'.join(parts)) 413 | 414 | def blame_link(repo, ref, path, file=None): 415 | parts = [x for x in (ref, path, file) if x] 416 | return url_for('blame', repo=repo.name, path='/'.join(parts)) 417 | 418 | def history_link(repo, ref, path, file=None): 419 | parts = [x for x in (ref, path, file) if x] 420 | return url_for('history', repo=repo.name, path='/'.join(parts)) 421 | 422 | def file_icon(file): 423 | mode = getattr(file, 'filemode', stat.S_IFREG) 424 | if stat.S_ISDIR(mode): 425 | return "/static/folder_icon.png" 426 | if stat.S_IXUSR & mode: 427 | return "/static/script_icon.png" 428 | if stat.S_ISLNK(mode): 429 | return "/static/link_icon.png" 430 | if stat.S_ISGITLNK(mode): 431 | return "/static/repo_icon.png" 432 | return "/static/file_icon.png" 433 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chardet 2 | docutils 3 | flask 4 | jinja2 5 | markdown 6 | pygments 7 | whelk 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name = "goblet", 6 | version = "0.3.5", 7 | author = "Dennis Kaarsemaker", 8 | author_email = "dennis@kaarsemaker.net", 9 | url = "http://seveas.github.com/goblet", 10 | description = "Git web interface using libgit2 and flask", 11 | packages = ["goblet"], 12 | package_data = {'goblet': ['themes/*/*/*.*', 'themes/*/*/*/*']}, 13 | classifiers = [ 14 | 'Development Status :: 3 - Alpha', 15 | 'Environment :: Web Environment', 16 | 'Intended Audience :: Developers', 17 | 'Intended Audience :: System Administrators', 18 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 19 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 20 | 'Topic :: Software Development', 21 | 'Topic :: Software Development :: Version Control', 22 | ] 23 | ) 24 | --------------------------------------------------------------------------------