#
#
# delete "about.psp"
#
# delete "branch.psp"
#
# delete "diff.psp"
#
# delete "enscriptlangs.py"
#
# delete "error.psp"
#
# delete "file.psp"
#
# delete "fileinbranch.psp"
#
# delete "headofbranch.psp"
#
# delete "help.psp"
#
# delete "html.py"
#
# delete "index.psp"
#
# delete "manifest.psp"
#
# delete "mimetypes/gnome-fs-directory.png"
#
# delete "mimetypes/gnome-library.png"
#
# delete "mimetypes/gnome-mime-application-magicpoint.png"
#
# delete "mimetypes/gnome-mime-application-msword.png"
#
# delete "mimetypes/gnome-mime-application-ogg.png"
#
# delete "mimetypes/gnome-mime-application-par.png"
#
# delete "mimetypes/gnome-mime-application-pdf.png"
#
# delete "mimetypes/gnome-mime-application-pgp-encrypted.png"
#
# delete "mimetypes/gnome-mime-application-pgp-keys.png"
#
# delete "mimetypes/gnome-mime-application-pgp.png"
#
# delete "mimetypes/gnome-mime-application-postscript.png"
#
# delete "mimetypes/gnome-mime-application-qif.png"
#
# delete "mimetypes/gnome-mime-application-rhythmbox-effect.png"
#
# delete "mimetypes/gnome-mime-application-rhythmbox-playlist.png"
#
# delete "mimetypes/gnome-mime-application-rtf.png"
#
# delete "mimetypes/gnome-mime-application-smil.png"
#
# delete "mimetypes/gnome-mime-application-vnd.lotus-1-2-3.png"
#
# delete "mimetypes/gnome-mime-application-vnd.ms-excel.png"
#
# delete "mimetypes/gnome-mime-application-vnd.ms-powerpoint.png"
#
# delete "mimetypes/gnome-mime-application-vnd.ms-word.png"
#
# delete "mimetypes/gnome-mime-application-vnd.stardivision.calc.png"
#
# delete "mimetypes/gnome-mime-application-vnd.stardivision.impress.png"
#
# delete "mimetypes/gnome-mime-application-vnd.stardivision.writer.png"
#
# delete "mimetypes/gnome-mime-application-vnd.sun.xml.calc.png"
#
# delete "mimetypes/gnome-mime-application-vnd.sun.xml.draw.png"
#
# delete "mimetypes/gnome-mime-application-vnd.sun.xml.impress.png"
#
# delete "mimetypes/gnome-mime-application-vnd.sun.xml.writer.png"
#
# delete "mimetypes/gnome-mime-application-vnd.sun.xml.writer.template.png"
#
# delete "mimetypes/gnome-mime-application-wordperfect.png"
#
# delete "mimetypes/gnome-mime-application-x-abiword.png"
#
# delete "mimetypes/gnome-mime-application-x-applix-spreadsheet.png"
#
# delete "mimetypes/gnome-mime-application-x-applix-word.png"
#
# delete "mimetypes/gnome-mime-application-x-archive.png"
#
# delete "mimetypes/gnome-mime-application-x-arj.png"
#
# delete "mimetypes/gnome-mime-application-x-bittorrent.png"
#
# delete "mimetypes/gnome-mime-application-x-bla.png"
#
# delete "mimetypes/gnome-mime-application-x-blender.png"
#
# delete "mimetypes/gnome-mime-application-x-blf.png"
#
# delete "mimetypes/gnome-mime-application-x-blv.png"
#
# delete "mimetypes/gnome-mime-application-x-bzip-compressed-tar.png"
#
# delete "mimetypes/gnome-mime-application-x-bzip.png"
#
# delete "mimetypes/gnome-mime-application-x-cd-image.png"
#
# delete "mimetypes/gnome-mime-application-x-class-file.png"
#
# delete "mimetypes/gnome-mime-application-x-compress.png"
#
# delete "mimetypes/gnome-mime-application-x-compressed-tar.png"
#
# delete "mimetypes/gnome-mime-application-x-core.png"
#
# delete "mimetypes/gnome-mime-application-x-cpio-compressed.png"
#
# delete "mimetypes/gnome-mime-application-x-cpio.png"
#
# delete "mimetypes/gnome-mime-application-x-dc-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-deb.png"
#
# delete "mimetypes/gnome-mime-application-x-desktop.png"
#
# delete "mimetypes/gnome-mime-application-x-dia-diagram.png"
#
# delete "mimetypes/gnome-mime-application-x-dv.png"
#
# delete "mimetypes/gnome-mime-application-x-dvi.png"
#
# delete "mimetypes/gnome-mime-application-x-e-theme.png"
#
# delete "mimetypes/gnome-mime-application-x-executable.png"
#
# delete "mimetypes/gnome-mime-application-x-extension-nfo.png"
#
# delete "mimetypes/gnome-mime-application-x-extension-par2.png"
#
# delete "mimetypes/gnome-mime-application-x-font-afm.png"
#
# delete "mimetypes/gnome-mime-application-x-font-bdf.png"
#
# delete "mimetypes/gnome-mime-application-x-font-linux-psf.png"
#
# delete "mimetypes/gnome-mime-application-x-font-pcf.png"
#
# delete "mimetypes/gnome-mime-application-x-font-sunos-news.png"
#
# delete "mimetypes/gnome-mime-application-x-font-ttf.png"
#
# delete "mimetypes/gnome-mime-application-x-gameboy-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-genesis-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-glade.png"
#
# delete "mimetypes/gnome-mime-application-x-gnucash.png"
#
# delete "mimetypes/gnome-mime-application-x-gnumeric.png"
#
# delete "mimetypes/gnome-mime-application-x-gtktalog.png"
#
# delete "mimetypes/gnome-mime-application-x-gzip.png"
#
# delete "mimetypes/gnome-mime-application-x-ipod-firmware.png"
#
# delete "mimetypes/gnome-mime-application-x-jar.png"
#
# delete "mimetypes/gnome-mime-application-x-killustrator.png"
#
# delete "mimetypes/gnome-mime-application-x-kpresenter.png"
#
# delete "mimetypes/gnome-mime-application-x-kspread.png"
#
# delete "mimetypes/gnome-mime-application-x-kword.png"
#
# delete "mimetypes/gnome-mime-application-x-lha.png"
#
# delete "mimetypes/gnome-mime-application-x-lhz.png"
#
# delete "mimetypes/gnome-mime-application-x-mrproject.png"
#
# delete "mimetypes/gnome-mime-application-x-msx-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-n64-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-nes-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-object.png"
#
# delete "mimetypes/gnome-mime-application-x-perl.png"
#
# delete "mimetypes/gnome-mime-application-x-php.png"
#
# delete "mimetypes/gnome-mime-application-x-python-bytecode.png"
#
# delete "mimetypes/gnome-mime-application-x-python.png"
#
# delete "mimetypes/gnome-mime-application-x-qw.png"
#
# delete "mimetypes/gnome-mime-application-x-rar.png"
#
# delete "mimetypes/gnome-mime-application-x-reject.png"
#
# delete "mimetypes/gnome-mime-application-x-rpm.png"
#
# delete "mimetypes/gnome-mime-application-x-ruby.png"
#
# delete "mimetypes/gnome-mime-application-x-sharedlib.png"
#
# delete "mimetypes/gnome-mime-application-x-shellscript.png"
#
# delete "mimetypes/gnome-mime-application-x-shockwave-flash.png"
#
# delete "mimetypes/gnome-mime-application-x-sms-rom.png"
#
# delete "mimetypes/gnome-mime-application-x-sql.png"
#
# delete "mimetypes/gnome-mime-application-x-stuffit.png"
#
# delete "mimetypes/gnome-mime-application-x-tar.png"
#
# delete "mimetypes/gnome-mime-application-x-tex.png"
#
# delete "mimetypes/gnome-mime-application-x-trash.png"
#
# delete "mimetypes/gnome-mime-application-x-x509-ca-cert.png"
#
# delete "mimetypes/gnome-mime-application-zip.png"
#
# delete "mimetypes/gnome-mime-application.png"
#
# delete "mimetypes/gnome-mime-audio-ac3.png"
#
# delete "mimetypes/gnome-mime-audio-basic.png"
#
# delete "mimetypes/gnome-mime-audio-midi.png"
#
# delete "mimetypes/gnome-mime-audio-x-aiff.png"
#
# delete "mimetypes/gnome-mime-audio-x-it.png"
#
# delete "mimetypes/gnome-mime-audio-x-midi.png"
#
# delete "mimetypes/gnome-mime-audio-x-mod.png"
#
# delete "mimetypes/gnome-mime-audio-x-mp3.png"
#
# delete "mimetypes/gnome-mime-audio-x-s3m.png"
#
# delete "mimetypes/gnome-mime-audio-x-stm.png"
#
# delete "mimetypes/gnome-mime-audio-x-ulaw.png"
#
# delete "mimetypes/gnome-mime-audio-x-voc.png"
#
# delete "mimetypes/gnome-mime-audio-x-wav.png"
#
# delete "mimetypes/gnome-mime-audio-x-xi.png"
#
# delete "mimetypes/gnome-mime-audio-x-xm.png"
#
# delete "mimetypes/gnome-mime-audio.png"
#
# delete "mimetypes/gnome-mime-image-bmp.png"
#
# delete "mimetypes/gnome-mime-image-gif.png"
#
# delete "mimetypes/gnome-mime-image-jpeg.png"
#
# delete "mimetypes/gnome-mime-image-png.png"
#
# delete "mimetypes/gnome-mime-image-svg+xml.png"
#
# delete "mimetypes/gnome-mime-image-svg.png"
#
# delete "mimetypes/gnome-mime-image-tiff.png"
#
# delete "mimetypes/gnome-mime-image-wmf.png"
#
# delete "mimetypes/gnome-mime-image-x-3ds.png"
#
# delete "mimetypes/gnome-mime-image-x-applix-graphic.png"
#
# delete "mimetypes/gnome-mime-image-x-cmu-raster.png"
#
# delete "mimetypes/gnome-mime-image-x-lwo.png"
#
# delete "mimetypes/gnome-mime-image-x-lws.png"
#
# delete "mimetypes/gnome-mime-image-x-xcf.png"
#
# delete "mimetypes/gnome-mime-image.png"
#
# delete "mimetypes/gnome-mime-text-css.png"
#
# delete "mimetypes/gnome-mime-text-html.png"
#
# delete "mimetypes/gnome-mime-text-x-authors.png"
#
# delete "mimetypes/gnome-mime-text-x-c++src.png"
#
# delete "mimetypes/gnome-mime-text-x-c-header.png"
#
# delete "mimetypes/gnome-mime-text-x-c.png"
#
# delete "mimetypes/gnome-mime-text-x-chdr.png"
#
# delete "mimetypes/gnome-mime-text-x-copying.png"
#
# delete "mimetypes/gnome-mime-text-x-credits.png"
#
# delete "mimetypes/gnome-mime-text-x-csh.png"
#
# delete "mimetypes/gnome-mime-text-x-csharp.png"
#
# delete "mimetypes/gnome-mime-text-x-csrc.png"
#
# delete "mimetypes/gnome-mime-text-x-haskell.png"
#
# delete "mimetypes/gnome-mime-text-x-install.png"
#
# delete "mimetypes/gnome-mime-text-x-java.png"
#
# delete "mimetypes/gnome-mime-text-x-literate-haskell.png"
#
# delete "mimetypes/gnome-mime-text-x-lyx.png"
#
# delete "mimetypes/gnome-mime-text-x-makefile.png"
#
# delete "mimetypes/gnome-mime-text-x-objcsrc.png"
#
# delete "mimetypes/gnome-mime-text-x-patch.png"
#
# delete "mimetypes/gnome-mime-text-x-readme.png"
#
# delete "mimetypes/gnome-mime-text-x-scheme.png"
#
# delete "mimetypes/gnome-mime-text-x-sql.png"
#
# delete "mimetypes/gnome-mime-text-x-tex.png"
#
# delete "mimetypes/gnome-mime-text-x-troff-man.png"
#
# delete "mimetypes/gnome-mime-text-x-txt.png"
#
# delete "mimetypes/gnome-mime-text-x-vcalendar.png"
#
# delete "mimetypes/gnome-mime-text-x-vcard.png"
#
# delete "mimetypes/gnome-mime-text-x-zsh.png"
#
# delete "mimetypes/gnome-mime-text-xml.png"
#
# delete "mimetypes/gnome-mime-text.png"
#
# delete "mimetypes/gnome-mime-video-mpeg.png"
#
# delete "mimetypes/gnome-mime-video-quicktime.png"
#
# delete "mimetypes/gnome-mime-video-x-ms-asf.png"
#
# delete "mimetypes/gnome-mime-video-x-ms-wmv.png"
#
# delete "mimetypes/gnome-mime-video-x-msvideo.png"
#
# delete "mimetypes/gnome-mime-video.png"
#
# delete "mimetypes/gnome-mime-x-directory-nfs-server.png"
#
# delete "mimetypes/gnome-mime-x-directory-smb-server.png"
#
# delete "mimetypes/gnome-mime-x-directory-smb-share.png"
#
# delete "mimetypes/gnome-mime-x-directory-smb-workgroup.png"
#
# delete "mimetypes/gnome-mime-x-font-afm.png"
#
# delete "mimetypes/gnome-package.png"
#
# delete "mimetypes/openofficeorg-19-database.png"
#
# delete "mimetypes/openofficeorg-19-drawing-template.png"
#
# delete "mimetypes/openofficeorg-19-drawing.png"
#
# delete "mimetypes/openofficeorg-19-formula.png"
#
# delete "mimetypes/openofficeorg-19-master-document.png"
#
# delete "mimetypes/openofficeorg-19-oasis-database.png"
#
# delete "mimetypes/openofficeorg-19-oasis-drawing-template.png"
#
# delete "mimetypes/openofficeorg-19-oasis-drawing.png"
#
# delete "mimetypes/openofficeorg-19-oasis-formula.png"
#
# delete "mimetypes/openofficeorg-19-oasis-master-document.png"
#
# delete "mimetypes/openofficeorg-19-oasis-presentation-template.png"
#
# delete "mimetypes/openofficeorg-19-oasis-presentation.png"
#
# delete "mimetypes/openofficeorg-19-oasis-spreadsheet-template.png"
#
# delete "mimetypes/openofficeorg-19-oasis-spreadsheet.png"
#
# delete "mimetypes/openofficeorg-19-oasis-text-template.png"
#
# delete "mimetypes/openofficeorg-19-oasis-text.png"
#
# delete "mimetypes/openofficeorg-19-oasis-web-template.png"
#
# delete "mimetypes/openofficeorg-19-presentation-template.png"
#
# delete "mimetypes/openofficeorg-19-presentation.png"
#
# delete "mimetypes/openofficeorg-19-spreadsheet-template.png"
#
# delete "mimetypes/openofficeorg-19-spreadsheet.png"
#
# delete "mimetypes/openofficeorg-19-text-template.png"
#
# delete "mimetypes/openofficeorg-19-text.png"
#
# delete "monotone.py"
#
# delete "revision.psp"
#
# delete "tags.psp"
#
# delete "tarofbranch.psp"
#
# delete "wrapper.py"
#
# rename "MochiKit"
# to "static/MochiKit"
#
# rename "builtpython.sh"
# to "release.sh"
#
# rename "rss_feed.gif"
# to "static/rss_feed.gif"
#
# rename "version.py"
# to "release.py"
#
# rename "viewmtn.css"
# to "static/viewmtn.css"
#
# rename "viewmtn.js"
# to "static/viewmtn.js"
#
# add_dir "fdo"
#
# add_dir "static"
#
# add_file "INSTALL"
# content [e6b186c3fbe5e1843af38b941f284add5e3ceaa6]
#
# add_file "fdo/__init__.py"
# content [da39a3ee5e6b4b0d3255bfef95601890afd80709]
#
# add_file "fdo/icontheme.py"
# content [9ec270a56c060a3c63b75a4f7d00c370a831ae29]
#
# add_file "fdo/sharedmimeinfo.py"
# content [dd993b9e526568246adc535ce4d47d360dd4eb3b]
#
# add_file "fdo/xdgbasedir.py"
# content [864ac96a21a2fe65c401a7beaf2c98513795751d]
#
# add_file "genproxy.py"
# content [408f46a3f5fe0d792eb62e92a8faaf5c28c67a54]
#
# add_file "mtn.py"
# content [10bae73eaa4b6b891d434bfcffa8ca66968b204b]
#
# add_file "static/highlight.css"
# content [45cedf6720b40c8e1cb9283d79e1929cb4e317c8]
#
# add_file "syntax.py"
# content [cf93e6e6a6166204daaae6ec50819ba175fa3ed1]
#
# add_file "templates/branch.html"
# content [9b84a47baffe133624433db124e92d70a206bb6a]
#
# add_file "templates/branchchanges.html"
# content [a8d275fa977d0714f41521f1409c3905ffe0236a]
#
# add_file "templates/branchchangesrss.html"
# content [2be7274e1a3348545e8378190418bf00e4eab73b]
#
# add_file "templates/branchchoosehead.html"
# content [e3233066d92401b9e6a0b6a8a20c2e3cc1aa383d]
#
# add_file "templates/help.html"
# content [f8faec929ff6f5fd1722240f12a8b398c88fb603]
#
# add_file "templates/revision.html"
# content [e9eeb6212f211ce522b1db16156e7129e00993d1]
#
# add_file "templates/revisionbrowse.html"
# content [a4175d3be3ff73d4e8d27667de546355fcece82d]
#
# add_file "templates/revisiondiff.html"
# content [06acbfdf78bebdc5ef8f0652278dfa829d49fbba]
#
# add_file "templates/revisionfile.html"
# content [652180e381d9e181020c3a4a52f5bc85cef8397d]
#
# add_file "templates/revisionfilebin.html"
# content [ed073e34de5e16ad7ed0d14ce01bff80b0d78970]
#
# add_file "templates/revisionfileimg.html"
# content [cd246dd75333b2ced5d8dd9216e0d50a16769dfd]
#
# add_file "templates/revisionfileobj.html"
# content [aecf0edca364646c23f9fecfb764159a73b4d7d1]
#
# add_file "templates/revisionfiletxt.html"
# content [8abafa4c4189b5c596ac59e6061d0ad116909393]
#
# add_file "templates/revisioninfo.html"
# content [05af137b031ca0fe6ff4d5200d3958a18d38de60]
#
# add_file "templates/tags.html"
# content [e37d06408dc812404695b1ac441426fa5c39e051]
#
# patch "TODO"
# from [6372d06a8696794b11e1f5dece588de4d1057896]
# to [9caf49f2ed434f3943a89b627d974c02f597ed49]
#
# patch "common.py"
# from [f77fd516022cfc46ebfc2070554f08eb36e1cee3]
# to [1462ca963d4b3959019bb0ac26a5311f6f2bd406]
#
# patch "config.py.example"
# from [7627e154e30ee6e88e547312876aa5cca8bf7c7e]
# to [a484f47e78d3b8aa4fcc875b44f1039220fb2837]
#
# patch "release.py"
# from [a071b2192e2092ec222ab0d59ef95efd1c9c81e6]
# to [9606208d767e8ed4999ca37fa5e20d13f3fa7be5]
#
# patch "release.sh"
# from [c1d18a362622ec209ea6818957a5778d5374de45]
# to [65fe70e241d026a9e09530245362cb09287b6608]
#
# patch "static/viewmtn.css"
# from [6efcadac0d56fb3d77a22786f477ba621f4af33d]
# to [8981409323f375efedb9599c206ac0b34bdad739]
#
# patch "templates/about.html"
# from [6b1c55564ae50e2de59d5eda418bfad86f0b596d]
# to [fea245cb17ab46a7766e03293800361db0b325b2]
#
# patch "templates/base.html"
# from [941ad9f0b2cc72c729f78185d140cce2f5088e29]
# to [96ce364d2dfd62b29e6557bebfd3e5f9a6c87b46]
#
# patch "templates/index.html"
# from [e0ae8e2a7cc89a8ff8d012742104ded01f6cf968]
# to [08145fdb8457c0f05ead0cdec4717e0f7f0de864]
#
# patch "viewmtn.py"
# from [b33f71afb2e598fadb81114ed5a569af368d390b]
# to [1dc5caa196fe72d70fa67baaad7ed79f3a430868]
#
# set "fdo/icontheme.py"
# attr "mtn:execute"
# value "true"
#
# set "fdo/sharedmimeinfo.py"
# attr "mtn:execute"
# value "true"
#
# set "genproxy.py"
# attr "mtn:execute"
# value "true"
#
# set "syntax.py"
# attr "mtn:execute"
# value "true"
#
============================================================
--- INSTALL e6b186c3fbe5e1843af38b941f284add5e3ceaa6
+++ INSTALL e6b186c3fbe5e1843af38b941f284add5e3ceaa6
@@ -0,0 +1,101 @@
+
+Installing ViewMTN
+------------------
+
+This document briefly describes what is necessary to install ViewMTN
+and configure a working installation.
+
+Dependencies
+------------
+
+ViewMTN should run on any Unix based operating system - MacOS,
+Linux, etc. It has not been ported to Windows, although that port
+should be possible.
+
+Monotone: http://www.venge.net/monotone/
+A version which is descended from [62961c1dc..] is required.
+This is post-0.30
+
+Python: http://www.python.org/
+A version >= 2.4 is required.
+
+Cheetah templates: http://www.cheetahtemplate.org/
+Version 0.9.16-1 from Debian is known to work.
+
+Optional
+--------
+
+Highlight: http://www.andre-simon.de/
+Version 2.4 is required for highlight work.
+Highlight is required if source code is to be shown highlighted.
+
+Shared Mime Info: http://freedesktop.org/wiki/Software/shared-mime-info
+Version 0.19 is known to work, although there is a specification
+so older versions should be fine. Most distributions provide this
+info. Note that if you install this into a non-standard path,
+please export XDG_DATA_DIRS correctly (eg. XDG_DATA_DIRS=/opt/local/share)
+Without this package, ViewMTN will only perform extremely basic
+MIME type auto-detection.
+
+Icon Theme: http://www.freedesktop.org/software/icon-theme/
+Any version should be fine. If possible, use a distributor version
+of this package (Ubuntu is good!) as it will have a much richer
+selection of icons. See later in this file to find out how to
+configure ViewMTN to use icon themes.
+
+Configuring ViewMTN
+-------------------
+
+ViewMTN can run in standalone mode, or under a web server. It is
+recommended when first installing to ViewMTN to test it in standalone
+mode.
+
+If you have not already done so, copy "config.py.example" to
+"config.py". You will then need to edit "config.py" to suit your
+site; there are numerous comments in the file, so this shouldn't be
+too hard.
+
+You're then ready to run ViewMTN;
+ ./viewmtn.py
+If you leave off the argument, ViewMTN will bind to port 8080.
+You can access ViewMTN by visiting:
+ http://localhost:8080/
+
+If everything has gone well, you should get the normal ViewMTN front
+page - a list of branches in the Monotone database you specified.
+If not, look at the output of viewmtn.py on the console; perhaps it
+cannot read your monotone database, the path to 'mtn' is wrong, etc.
+
+Running ViewMTN in a web server
+-------------------------------
+
+ViewMTN is based upon web.py (http://webpy.org/); the installation
+instructions of webpy thus work for viewmtn. They are available
+here:
+ http://webpy.infogami.com/install
+
+The following snippet of configuration is used to configure ViewMTN
+on http://viewmtn.angrygoats.net/ (running lighttpd) and is therefore
+known to work. You should be able to use it (with adjustment to
+suit your site).
+
+ fastcgi.server = ( "/viewmtn.py" =>
+ (( "socket" => "/var/tmp/lighttpd/viewmtn.socket",
+ "bin-path" => "/home/grahame/mtn/viewmtn/viewmtn.py",
+ "max-procs" => 8,
+ "check-local" => "disable",
+ ))
+ )
+
+ ## a static document-root, for virtual-hosting take look at the
+ ## server.virtual-* options
+ $HTTP["host"] == "viewmtn.angrygoats.net" {
+ server.document-root = "/home/grahame/mtn/viewmtn/"
+ dir-listing.activate = "enable"
+
+ url.rewrite-once = (
+ "^/favicon.ico$" => "/static/favicon.ico",
+ "^/static/(.*)$" => "/static/$1",
+ "^/(.*)$" => "/viewmtn.py/$1",
+ )
+ }
============================================================
--- fdo/__init__.py da39a3ee5e6b4b0d3255bfef95601890afd80709
+++ fdo/__init__.py da39a3ee5e6b4b0d3255bfef95601890afd80709
============================================================
--- fdo/icontheme.py 9ec270a56c060a3c63b75a4f7d00c370a831ae29
+++ fdo/icontheme.py 9ec270a56c060a3c63b75a4f7d00c370a831ae29
@@ -0,0 +1,131 @@
+#!/usr/bin/env python2.4
+
+# an implementation of:
+# http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
+# (or at least, enough of it to do mime_type->icon file conversion; more could
+# be added later if someone was keen..)
+
+from ConfigParser import ConfigParser
+import xdgbasedir
+import os
+
+class IconTheme:
+ def __init__(self, icon_theme):
+ self.icon_theme = icon_theme
+ # list of directories in order, in which to look for files
+ self.locs = []
+ home = os.getenv('HOME')
+ if home:
+ self.locs.append(os.path.join(home, '.icons'))
+ for path in xdgbasedir.xdg_data_dirs():
+ self.locs.append(os.path.join(path, 'icons'))
+ self.locs.append('/usr/share/pixmaps')
+ self.locs = filter(lambda x: os.access(x, os.R_OK), self.locs)
+
+ # find our index.theme file
+ index_theme_file = None
+ for path in self.locs:
+ ini = os.path.join(path, icon_theme, 'index.theme')
+ if os.access(ini, os.R_OK):
+ index_theme_file = ini
+ break
+
+ if not index_theme_file:
+ raise Exception("Unable to load index.theme for theme %s" % icon_theme)
+
+ self.cp = cp = ConfigParser()
+ cp.read(index_theme_file)
+ it = "Icon Theme"
+ if not cp.has_section(it):
+ raise Exception("Theme does not have an 'Icon Theme' section.")
+ if not cp.has_option(it, "Directories"):
+ raise Exception("Theme does not specify its directories.")
+ else:
+ self.directories = cp.get(it, "Directories").split(',')
+ if cp.has_option(it, "Inherits"):
+ self.inherits = cp.get(it, "Inherits").split(',')
+ elif icon_theme != 'hicolor':
+ self.inherits = ['hicolor']
+ else:
+ self.inherits = []
+ self.inherits = map(IconTheme, self.inherits)
+
+ # most of the time, we'll care about directories by type and
+ # then by size
+ self.dir_by_size = {}
+ for path in self.directories:
+ context = cp.get(path, 'Context', None)
+ if not cp.has_option(path, 'Size'):
+ raise Exception("Directory '%s' has no size." % (path))
+ size = cp.get(path, 'Size')
+ self.dir_by_size.setdefault(context, {}).setdefault(size, []).append(path)
+
+ def lookup(self, icon_name, contexts=None, size=None, accept_extensions=None):
+ if accept_extensions == None:
+ accept_extensions = ["png", "xpm", "svg"]
+ to_scan = []
+ if contexts == None:
+ contexts = self.dir_by_size.keys()
+
+ for context in contexts:
+ if not self.dir_by_size.has_key(context):
+ continue
+ for dir_size in self.dir_by_size[context]:
+ if size == None or size == dir_size:
+ for path in self.dir_by_size[context][dir_size]:
+ for loc in self.locs:
+ for ext in accept_extensions:
+ attempt = os.path.join(loc, self.icon_theme, path, icon_name+'.'+ext)
+ #print "attempt:", attempt
+ if os.access(attempt, os.R_OK):
+ #print "success!"
+ return attempt
+
+ # if we get here, try our parent themes
+ for it in self.inherits:
+ rv = it.lookup(icon_name, contexts, size, accept_extensions)
+ if rv: return rv
+ return None
+
+class MimeIcon:
+ def __init__(self, icon_theme, size=None):
+ self.icon_theme = icon_theme
+ self.size = size
+ self.cache = {}
+
+ def lookup(self, mime_type):
+ def __lookup():
+ def gnome_mime(mime_type):
+ return "gnome-mime-" + mime_type.replace('/', '-')
+ # iff we are a inode/ type, then let's look in 'Places' instead
+ if mime_type.startswith('inode/'):
+ gnome_name = 'gnome-fs-' + mime_type.split('/')[-1]
+ rv = self.icon_theme.lookup(gnome_name, contexts=['Places', 'FileSystems'], size=self.size)
+ if rv: return rv
+ # stolen from gnome-ui; it's rather unfortunate but there is not a standard
+ # on how to name mime icons!
+ # http://cvs.gnome.org/viewcvs/libgnomeui/libgnomeui/gnome-icon-lookup.c
+ gnome_name = gnome_mime(mime_type)
+ rv = self.icon_theme.lookup(gnome_name, contexts=['MimeTypes'], size=self.size)
+ if rv: return rv
+ # try the x-generic stuff
+ generic_name = mime_type.split('/')[0] + '-x-generic'
+ rv = self.icon_theme.lookup(generic_name, contexts=['MimeTypes'], size=self.size)
+ if rv: return rv
+ # otherwise, this seems to work some of the time
+ gnome_name = gnome_mime(mime_type.split('/')[0])
+ rv = self.icon_theme.lookup(gnome_name, contexts=['MimeTypes'], size=self.size)
+ if rv: return rv
+ # otherwise, otherwise, one of these should work\n
+ really_fallbacks = [(['Applications'], 'gnome-unknown'), (['MimeTypes'], 'unknown')]
+ for contexts, icon_name in really_fallbacks:
+ rv = self.icon_name.lookup(icon_name, contexts=contexts, size=self.size)
+ if rv: return rv
+ if not self.cache.has_key(mime_type):
+ self.cache[mime_type] = __lookup()
+ return self.cache[mime_type]
+
+if __name__ == '__main__':
+ it = IconTheme('gnome')
+ mi = MimeIcon(it, size="16")
+ print mi.lookup('inode/directory')
============================================================
--- fdo/sharedmimeinfo.py dd993b9e526568246adc535ce4d47d360dd4eb3b
+++ fdo/sharedmimeinfo.py dd993b9e526568246adc535ce4d47d360dd4eb3b
@@ -0,0 +1,295 @@
+#!/usr/bin/env python2.4
+
+# written to:
+# http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-0.13.html
+
+import xdgbasedir
+import os, re, sys, fnmatch
+
+def mime():
+ return [os.path.join(t, 'mime') for t in xdgbasedir.xdg_data_dirs()]
+
+class GlobLookup(object):
+ def __init__(self):
+ self.literal = {}
+ self.complex = {}
+ self.extensions = {}
+ mime_dirs = mime()
+ for precedence, dir in enumerate(mime_dirs):
+ mime_file = os.path.join(dir, 'globs')
+ if os.access(mime_file, os.R_OK):
+ self.update(precedence, mime_file)
+ self.complex_by_size = self.complex.keys()
+ self.complex_by_size.sort(lambda b, a: cmp(len(a), len(b)))
+
+ def update(self, precedence, filename):
+ literal = re.compile(r'^[^\[\*\?]+$')
+ simple_glob = re.compile(r'^\*\.([^\[\*\?]+)$')
+ for line in (t.rstrip('\n') for t in open(filename)):
+ if line.startswith('#'):
+ continue
+ mimetype, glob = line.split(':')
+ if literal.match(glob):
+ self.add_literal(precedence, glob, mimetype)
+ continue
+ m = simple_glob.match(glob)
+ if m:
+ self.add_extension(precedence, m.groups()[0], mimetype)
+ else:
+ self.add_complex(precedence, glob, mimetype)
+
+ def add_literal(self, precedence, glob, mimetype):
+ self.literal.setdefault(glob, []).append((precedence, mimetype))
+
+ def add_extension(self, precedence, extension, mimetype):
+ self.extensions.setdefault(extension, []).append((precedence, mimetype))
+
+ def add_complex(self, precedence, glob, mimetype):
+ self.complex.setdefault(glob, []).append((precedence, mimetype))
+
+ def __lookup(self, filename):
+ # increasing; lower is higher priority
+ precedence_sort = lambda a,b: cmp(a[0], b[0])
+ def best_of(match_list):
+ match_list.sort(precedence_sort)
+ return match_list[0][1]
+ if self.literal.has_key(filename):
+ return best_of(self.literal[filename])
+ for complex in self.complex_by_size:
+ if fnmatch.fnmatch(filename, complex):
+ return best_of(self.complex[complex])
+ # find potential extensions in filename
+ def extensions():
+ ext = filename
+ while ext:
+ idx = ext.find('.')
+ if idx == -1:
+ break
+ ext = ext[idx+1:]
+ yield ext
+ for extension in extensions():
+ if self.extensions.has_key(extension):
+ return best_of(self.extensions[extension])
+ return None
+
+ def lookup(self, filename):
+ rv = self.__lookup(filename)
+ if rv: return rv
+ filename = filename.lower()
+ return self.__lookup(filename)
+
+class MagicLookup(object):
+ def __init__(self):
+ # hashed by priority, then by mime type, then a list of headers
+ # (multiple files could easily have conflicts otherwise)
+ self.headers = {}
+ mime_dirs = mime()
+ for dir in mime_dirs:
+ magic_file = os.path.join(dir, 'magic')
+ if os.access(magic_file, os.R_OK):
+ self.update(magic_file)
+
+ def update(self, fname):
+ fd = open(fname, 'rb')
+ if fd.readline() != 'MIME-Magic\0\n':
+ raise Exception("Not a Mime Magic file: %s" % fname)
+
+ header_re = re.compile(r'^\[([0-9]+):([^\]]+)\]$')
+ value_re = re.compile(r'^([0-9]*)>([0-9]+)=')
+ options_re = re.compile(r'^(\~[0-9]+)?(\+[0-9]+)?$')
+
+ # read a header, followed by a number of lines
+ self.__buf = ''
+
+ def skip_line():
+ nl = self.__buf.find('\n')
+ if nl == -1:
+ self.__buf = ''
+ else:
+ self.__buf = self.__buf[nl+1:]
+
+ def read_header():
+ m = header_re.match(self.__buf)
+ if not m:
+ skip_line()
+ else:
+ self.__buf = self.__buf[m.end()+1:]
+ priority, mime_type = m.groups()
+ return int(priority), mime_type
+
+ def read_line():
+ # the next line will have indent, start_offset and the
+ # start of the value
+ m = value_re.match(self.__buf)
+ if not m:
+ skip_line()
+ return
+ indent, start_offset = m.groups()
+ try: indent = int(indent)
+ except: indent = 0
+ try: start_offset = int(start_offset)
+ except: start_offset = 0
+ self.__buf = self.__buf[m.end():]
+ # the next two bytes are the length, in big-endian format
+ length = (ord(self.__buf[0]) << 8) + ord(self.__buf[1])
+ self.__buf = self.__buf[2:]
+ # read the remainder of the value
+ to_read = length - len(self.__buf)
+ if to_read > 0:
+ self.__buf += fd.read(to_read)
+ value, self.__buf = self.__buf[:length], self.__buf[length:]
+ # is the next thing a mask?
+ if len(self.__buf) == 0:
+ self.__buf += fd.read(1)
+ if self.__buf[0] == '&':
+ self.__buf = self.__buf[1:]
+ to_read = length - len(self.__buf)
+ if to_read > 0:
+ self.__buf += fd.read(to_read)
+ mask, self.__buf = self.__buf[:length], self.__buf[length:]
+ else:
+ mask = '\xff' * length
+ # anything remaining will end in a newline; so readline()
+ # does what we want. see whether or not we need to call it..
+ if len(self.__buf) == 0 or self.__buf[-1] != '\n':
+ self.__buf += fd.readline()
+ word_size, range_length = 1, 1
+ m = options_re.match(self.__buf)
+ if m:
+ for group in m.groups():
+ if not group:
+ continue
+ if group[0] == '~':
+ word_size = int(group[1:])
+ elif group[0] == '+':
+ range_length = int(group[1:])
+ self.__buf = self.__buf[m.end()+1:]
+ # fix the byte order, on little-endian systems
+ if sys.byteorder == 'little' and word_size > 1:
+ if len(value) % word_size != 0:
+ raise Exception("value is not an integer multiple of word size!")
+ # make your sanity save now!
+ fix = lambda x: ''.join([''.join(reversed(value[t:t+word_size])) for t in xrange(0,len(x)/word_size,word_size)])
+ value = fix(value)
+ mask = fix(mask)
+ return { 'indent' : indent,
+ 'start_offset' : start_offset,
+ 'value' : value,
+ 'mask' : mask,
+ 'range_length' : range_length,
+ 'word_size' : word_size }
+
+ current_header = None
+ while True:
+ if self.__buf == '':
+ nd = fd.readline()
+ if not nd:
+ break
+ self.__buf += nd
+# print "loop:", repr(self.__buf)
+ if self.__buf[0] == '[':
+ priority, mime_type = read_header()
+ current_header = []
+ self.headers.setdefault(priority, []).append((mime_type, current_header))
+ line_stack = []
+ else:
+ if current_header == None:
+ raise Exception("non-header before header!")
+ line = read_line()
+ current_header.append(line)
+
+ def lookup(self, data, priorities=None):
+ def match_line(line):
+ data_size = len(data)
+ value_size = len(line['value'])
+
+ def match_with(data_chunk):
+ if line['mask'] != None:
+ masked = ''.join([chr(ord(t) & ord(line['mask'][i])) for i, t in enumerate(data_chunk)])
+ else:
+ masked = data_chunk
+ if masked == line['value']:
+ return True
+
+ for i in range(line['range_length']):
+ from_offset = line['start_offset'] + i
+ to_offset = from_offset + value_size
+ if to_offset > data_size:
+ continue
+ if match_with(data[from_offset:to_offset]):
+ return True
+
+ def match_lines(lines):
+ # we need to maintain a current indent depth; we don't need to
+ # actually care what our parent was, as if we made it to checking
+ # we have necessarily succeeded
+ depth = -1
+ length = len(lines)
+ for idx, line in enumerate(lines):
+ indent = line['indent']
+ if indent > depth+1:
+ continue
+ if match_line(line):
+ if (idx+1 == length) or lines[idx+1]['indent'] <= indent:
+ # this is a match by itself
+ return True
+ depth = indent
+ elif indent <= depth:
+ depth = indent - 1
+
+ if not priorities:
+ priorities = self.priorities()
+ for priority in priorities:
+ for mime_type, lines in self.headers[priority]:
+ if match_lines(lines):
+ return mime_type
+ return None
+
+ def priorities(self):
+ rv = self.headers.keys()
+ rv.sort()
+ rv.reverse()
+ return rv
+
+class LookupHelper:
+ def __init__(self):
+ self.glob_lookup = GlobLookup()
+ self.magic_lookup = MagicLookup()
+ nontext_chars = "\x01\x02\x03\x04\x05\x06\x0e\x0f"\
+ "\x10\x11\x12\x13\x14\x15\x16\x17"\
+ "\x18\x19\x1a\x1c\x1d\x1e\x1f"
+ self.nontext = {}
+ for char in nontext_chars:
+ self.nontext[char] = True
+
+ def is_binary(self, str):
+ for char in str:
+ if self.nontext.has_key(char):
+ return True
+ return False
+
+ def lookup(self, filename, data):
+ # spec says we try >= 80 priority magic matchers, then filename, then the other matchers
+ threshold = 80
+ priorities = self.magic_lookup.priorities()
+ rv = self.magic_lookup.lookup(data, [t for t in priorities if t >= threshold])
+ if rv != None:
+ return rv
+ # then try guessing from filename
+ rv = self.glob_lookup.lookup(filename)
+ if rv != None:
+ return rv
+ # then try the other magic matchers
+ rv = self.magic_lookup.lookup(data, [t for t in priorities if t < threshold])
+ if rv != None:
+ return rv
+ # okay; fall-back behaviour is to return text/plain iff ! is_binary,
+ # otherwise application/octet-stream
+ if self.is_binary(data):
+ return 'application/octet-stream'
+ else:
+ return 'text/plain'
+
+if __name__ == '__main__':
+ c = LookupHelper()
+ print c.lookup('test.tar', '')
============================================================
--- fdo/xdgbasedir.py 864ac96a21a2fe65c401a7beaf2c98513795751d
+++ fdo/xdgbasedir.py 864ac96a21a2fe65c401a7beaf2c98513795751d
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+#
+# An implementation of the XDG Base Directory Specification
+# http://standards.freedesktop.org/basedir-spec/latest/
+#
+
+import os
+
+def xdg_data_home():
+ rv = os.getenv('XDG_DATA_HOME')
+ if rv:
+ return rv
+ home = os.getenv('HOME')
+ if home:
+ return os.path.join(home, ".local", "share")
+ else:
+ raise Exception("Unable to determine xdg_data_home")
+
+def xdg_config_home():
+ rv = os.getenv('XDG_CONFIG_HOME')
+ if rv:
+ return rv
+ home = os.getenv('HOME')
+ if home:
+ return os.path.join(home, ".config")
+ else:
+ raise Exception("Unable to determine xdg_config_home")
+
+def xdg_data_dirs():
+ dirs = [xdg_data_home()]
+ for dir in os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':'):
+ dirs.append(dir)
+ return dirs
+
+def xdg_config_dirs():
+ dirs = [xdg_config_home()]
+ for dir in os.getenv('XDG_DATA_DIRS', '/etc/xdg').split(':'):
+ dirs.append(dir)
+ return dirs
+
+def xdg_cache_home():
+ rv = os.getenv('XDG_CACHE_HOME')
+ if rv:
+ return rv
+ home = os.getenv('HOME')
+ if home:
+ return os.path.join(home, ".cache")
+ else:
+ raise Exception("Unable to determine xdg_cache_home")
============================================================
--- genproxy.py 408f46a3f5fe0d792eb62e92a8faaf5c28c67a54
+++ genproxy.py 408f46a3f5fe0d792eb62e92a8faaf5c28c67a54
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+class GeneratorProxy(object):
+ def __init__(self, generator):
+ self.generator = generator
+ def __iter__(self):
+ return self
+ def next(self):
+ return self.generator.next()
+
+class Seedy(GeneratorProxy):
+ def __del__(self):
+ print "testing"
+
+def test():
+ yield 2
+ yield 3
+ yield 4
+
+if __name__ == '__main__':
+ a = test()
+ b = Seedy(test())
+ for i in b:
+ print i
+
============================================================
--- mtn.py 10bae73eaa4b6b891d434bfcffa8ca66968b204b
+++ mtn.py 10bae73eaa4b6b891d434bfcffa8ca66968b204b
@@ -0,0 +1,407 @@
+
+import os
+import re
+import fcntl
+import pipes
+import select
+import threading
+import popen2
+from common import set_nonblocking, terminate_popen3
+from traceback import format_exc
+import genproxy
+
+import web
+from web import debug
+
+# regular expressions that are of general use when
+# validating monotone output
+def group_compile(r):
+ return re.compile('('+r+')')
+
+hex_re = r'[A-Fa-f0-9]*'
+hex_re_c = group_compile(hex_re)
+revision_re = r'[A-Fa-f0-9]{40}'
+revision_re_c = group_compile(revision_re)
+name_re = r'^[\S]+'
+name_re_c = group_compile(name_re)
+
+class MonotoneException(Exception):
+ pass
+
+class Revision(str):
+ def __init__(self, v):
+ # special case that must be handled: empty (initial) revision ID ''
+ str.__init__(v)
+ self.obj_type = "revision"
+ if v != '' and not revision_re_c.match(self):
+ raise MonotoneException("Not a valid revision ID: %s" % (v))
+ def abbrev(self):
+ return '[' + self[:8] + '..]'
+
+class Author(str):
+ def __init__(self, v):
+ str.__init__(v)
+ self.obj_type = "author"
+
+class Runner:
+ def __init__(self, monotone, database):
+ self.base_command = [monotone, "--db=%s" % pipes.quote(database)]
+
+packet_header_re = re.compile(r'^(\d+):(\d+):([lm]):(\d+):')
+import web
+
+class Automate(Runner):
+ """Runs commands via a particular monotone process. This
+ process is started the first time run() is called, and
+ stopped when this class instance is deleted or the stop()
+ method is called.
+
+ If an error occurs, the monotone process may need to be
+ stopped and a new one created.
+ """
+ def __init__(self, *args, **kwargs):
+ Runner.__init__(*[self] + list(args), **kwargs)
+ self.lock = threading.Lock()
+ self.process = None
+
+ def stop(self):
+ if not self.process:
+ return
+ terminate_popen3(self.process)
+ self.process = None
+
+ def __process_required(self):
+ if self.process != None:
+ return
+ to_run = self.base_command + ['automate', 'stdio']
+ self.process = popen2.Popen3(to_run, capturestderr=True)
+ map (set_nonblocking, [ self.process.fromchild,
+ self.process.tochild,
+ self.process.childerr ])
+
+ def run(self, *args, **kwargs):
+# debug(("automate is running:", args, kwargs))
+
+ lock = self.lock
+ stop = self.stop
+ class CleanRequest(genproxy.GeneratorProxy):
+ def __init__(self, *args, **kwargs):
+ genproxy.GeneratorProxy.__init__(self, *args, **kwargs)
+
+ # nb; this used to be False, but True seems to behave more sensibly.
+ # in particular, if someone holds down Refresh sometimes the code
+ # gets here before __del__ is called on the previous iterator,
+ # causing a pointless error to occur
+ if not lock.acquire(True):
+ # I've checked; this exception does _not_ cause __del__ to run, so
+ # we don't accidentally unlock a lock below
+ raise MonotoneException("Automate request cannot be called: it is already locked! This indicates a logic error in ViewMTN; please report.")
+
+ def __del__(self):
+ def read_any_unread_output():
+ try:
+ # this'll raise StopIteration if we're done
+ self.next()
+ # okay, we're not done..
+ debug("warning: Automate output not completely read; reading manually.")
+ for stanza in self:
+ pass
+ except StopIteration:
+ pass
+
+ try:
+ read_any_unread_output()
+ lock.release()
+ except:
+ debug("exception cleaning up after Automation; calling stop()!")
+ stop()
+
+ return CleanRequest(self.__run(*args, **kwargs))
+
+ def __run(self, command, args):
+ enc = "l%d:%s" % (len(command), command)
+ enc += ''.join(["%d:%s" % (len(x), x) for x in args]) + 'e'
+
+ # number of tries to get a working mtn going..
+ for i in xrange(2):
+ self.__process_required()
+ try:
+ self.process.tochild.write(enc)
+ self.process.tochild.flush()
+ break
+ except:
+ # mtn has died underneath the automate; restart it
+ debug("exception writing to child process; attempting restart: %s" % format_exc())
+ self.stop()
+
+ import sys
+ def read_result_packets():
+ buffer = ""
+ while True:
+ r_stdin, r_stdout, r_stderr = select.select([self.process.fromchild], [], [], None)
+ if not r_stdin and not r_stdout and not r_stderr:
+ break
+
+ if self.process.fromchild in r_stdin:
+ data = self.process.fromchild.read()
+ if data == "":
+ break
+ buffer += data
+
+ # loop, trying to get complete packets out of our buffer
+ complete, in_packet = False, False
+ while not complete and buffer != '':
+ if not in_packet:
+ m = packet_header_re.match(buffer)
+ if not m:
+ break
+ in_packet = True
+ cmdnum, errnum, pstate, length = m.groups()
+ errnum = int(errnum)
+ length = int(length)
+ header_length = m.end(m.lastindex) + 1 # the '1' is the colon
+
+ if len(buffer) < length + header_length:
+ # not enough data read from client yet; go round
+ break
+ else:
+ result = buffer[header_length:header_length+length]
+ buffer = buffer[header_length+length:]
+ complete = pstate == 'l'
+ in_packet = False
+ yield errnum, complete, result
+
+ if complete:
+ break
+
+ # get our response, and yield() it back one line at a time
+ code_max = -1
+ data_buf = ''
+ for code, is_last, data in read_result_packets():
+ if code and code > code_max:
+ code_max = code
+ data_buf += data
+ while True:
+ nl_idx = data_buf.find('\n')
+ if nl_idx == -1:
+ break
+ yield data_buf[:nl_idx+1]
+ data_buf = data_buf[nl_idx+1:]
+ # left over data?
+ if data_buf:
+ yield data_buf
+ if code_max > 0:
+ raise MonotoneException("error code %d in automate packet." % (code_max))
+
+class Standalone(Runner):
+ """Runs commands by running monotone. One monotone process
+ per command"""
+
+ def run(self, command, args):
+ # as we pass popen3 as sequence, it executes monotone with these
+ # arguments - and does not pass them through the shell according
+ # to help(os.popen3)
+# debug(("standalone is running:", command, args))
+ to_run = self.base_command + [command] + args
+ process = popen2.Popen3(to_run, capturestderr=True)
+ for line in process.fromchild:
+ yield line
+ stderr_data = process.childerr.read()
+ if len(stderr_data) > 0:
+ raise MonotoneException("data on stderr for command '%s': %s" % (command,
+ stderr_data))
+ terminate_popen3(process)
+
+class MtnObject:
+ def __init__(self, obj_type):
+ self.obj_type = obj_type
+
+class Tag(MtnObject):
+ def __init__(self, name, revision, author, branches):
+ MtnObject.__init__(self, "tag")
+ self.name, self.revision, self.author, self.branches = name, Revision(revision), author, branches
+
+class Branch(MtnObject):
+ def __init__(self, name):
+ MtnObject.__init__(self, "branch")
+ self.name = name
+
+class File(MtnObject):
+ def __init__(self, name, in_revision):
+ MtnObject.__init__(self, "file")
+ self.name = name
+ self.in_revision = in_revision
+
+class Dir(MtnObject):
+ def __init__(self, name, in_revision):
+ MtnObject.__init__(self, "dir")
+ self.name = name
+ self.in_revision = in_revision
+
+basic_io_name_tok = re.compile(r'^(\S+)')
+
+def basic_io_from_stream(gen):
+ # all of these x_consume functions return parsed string
+ # token to add to stanza, name of next consume function to call
+ # new value of line (eg. with consumed tokens removed)
+
+ def hex_consume(line):
+ m = hex_re_c.match(line[1:])
+ if line[0] != '[' or not m:
+ raise MonotoneException("This is not a hex token: %s" % line)
+ end_of_match = m.end(m.lastindex)
+ if line[end_of_match+1] != ']':
+ raise MonotoneException("Hex token ends in character other than ']': %s" % line)
+ return Revision(m.groups()[0]), choose_consume, line[end_of_match+2:]
+
+ def name_consume(line):
+ m = name_re_c.match(line)
+ if not m:
+ raise MonotoneException("Not a name: %s" % line)
+ end_of_match = m.end(m.lastindex)
+ return m.groups()[0], choose_consume, line[end_of_match:]
+
+ def choose_consume(line):
+ line = line.lstrip()
+ if line == '':
+ consumer = choose_consume
+ elif line[0] == '[':
+ consumer = hex_consume
+ elif line[0] == '"':
+ consumer = string_consume
+ else:
+ consumer = name_consume
+ return None, consumer, line
+
+ class StringState:
+ def __init__(self):
+ self.in_escape = False
+ self.has_started = False
+ self.has_ended = False
+ self.value = ''
+
+ def string_consume(line, state=None):
+ if not state:
+ state = StringState()
+
+ if not state.has_started:
+ if line[0] != '"':
+ raise MonotoneException("Not a string: %s" % line)
+ line = line[1:]
+ state.has_started = True
+
+ idx = 0
+ for idx, c in enumerate(line):
+ if state.in_escape:
+ if c != '\\' and c != '"':
+ raise MonotoneException("Invalid escape code: %s in %s\n" % (c, line))
+ state.value += c
+ state.in_escape = False
+ else:
+ if c == '\\':
+ state.in_escape = True
+ elif c == '"':
+ state.has_ended = True
+ break
+ else:
+ state.value += c
+
+ if state.has_ended:
+ return state.value, choose_consume, line[idx+1:]
+ else:
+ return (None,
+ lambda s: string_consume(s, state),
+ line[idx+1:])
+
+ consumer = choose_consume
+ current_stanza = []
+ for line in gen:
+ # if we're not in an actual consumer (which we shouldn't be, unless
+ # we're parsing some sort of multi-line token) and we have a blank
+ # line, it indicates the end of any current stanza
+ if (consumer == choose_consume) and (line == '' or line == '\n') and current_stanza:
+ yield current_stanza
+ current_stanza = []
+ continue
+
+ while line != '' and line != '\n':
+ new_token, consumer, line = consumer(line)
+ if new_token != None:
+ current_stanza.append(new_token)
+ if current_stanza:
+ yield current_stanza
+
+class Operations:
+ def __init__(self, runner_args):
+ self.standalone = apply(Standalone, runner_args)
+ self.automate = apply(Automate, runner_args)
+
+ def tags(self):
+ for stanza in basic_io_from_stream(self.automate.run('tags', [])):
+ if stanza[0] == 'tag':
+ branches = []
+ for branch in stanza[7:]:
+ branches.append(Branch(branch))
+ yield Tag(stanza[1], stanza[3], stanza[5], branches)
+
+ def branches(self):
+ for line in (t.strip() for t in self.automate.run('branches', [])):
+ if not line:
+ continue
+ yield apply(Branch, (line,))
+
+ def graph(self):
+ for line in self.automate.run('graph', []):
+ yield line
+
+ def parents(self, revision):
+ if revision != "":
+ for line in (t.strip() for t in self.automate.run('parents', [revision])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def children(self, revision):
+ if revision != "":
+ for line in (t.strip() for t in self.automate.run('children', [revision])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def toposort(self, revisions):
+ for line in (t.strip() for t in self.automate.run('toposort', revisions)):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def heads(self, branch):
+ for line in (t.strip() for t in self.automate.run('heads', [branch])):
+ if not line:
+ continue
+ yield apply(Revision, (line,))
+
+ def get_content_changed(self, revision, path):
+ for stanza in basic_io_from_stream(self.automate.run('get_content_changed', [revision, path])):
+ yield stanza
+
+ def get_revision(self, revision):
+ for stanza in basic_io_from_stream(self.automate.run('get_revision', [revision])):
+ yield stanza
+
+ def get_manifest_of(self, revision):
+ for stanza in basic_io_from_stream(self.automate.run('get_manifest_of', [revision])):
+ yield stanza
+
+ def get_file(self, fileid):
+ for stanza in self.automate.run('get_file', [fileid]):
+ yield stanza
+
+ def certs(self, revision):
+ for stanza in basic_io_from_stream(self.automate.run('certs', [revision])):
+ yield stanza
+
+ def diff(self, revision_from, revision_to, files=[]):
+ args = ['-r', revision_from, '-r', revision_to] + files
+ for line in self.standalone.run('diff', args):
+ yield line
+
============================================================
--- static/highlight.css 45cedf6720b40c8e1cb9283d79e1929cb4e317c8
+++ static/highlight.css 45cedf6720b40c8e1cb9283d79e1929cb4e317c8
@@ -0,0 +1,20 @@
+/* Style definition file generated by highlight 2.4.6, http://www.andre-simon.de/ */
+
+/* Highlighting theme definition: */
+
+body.hl { background-color:#ffffff; }
+pre.hl { color:#000000; background-color:#ffffff; font-size:10pt; font-family:Courier;}
+.num { color:#2928ff; }
+.esc { color:#ff00ff; }
+.str { color:#ff0000; }
+.dstr { color:#818100; }
+.slc { color:#838183; font-style:italic; }
+.com { color:#838183; font-style:italic; }
+.dir { color:#008200; }
+.sym { color:#000000; }
+.line { color:#555555; }
+.kwa { color:#000000; font-weight:bold; }
+.kwb { color:#830000; }
+.kwc { color:#000000; font-weight:bold; }
+.kwd { color:#010181; }
+
============================================================
--- syntax.py cf93e6e6a6166204daaae6ec50819ba175fa3ed1
+++ syntax.py cf93e6e6a6166204daaae6ec50819ba175fa3ed1
@@ -0,0 +1,91 @@
+#!/usr/bin/env python2.4
+
+from common import set_nonblocking, terminate_popen3
+from web import debug
+import config
+import popen2
+import select
+import cgi
+
+mime_to_lang = {
+
+}
+
+def highlight(*args, **kwargs):
+ # if we don't have a highlighter, let's just return the original
+ # iterator
+ if not hasattr(config, "highlight_command"):
+ return __basic_highlight(*args, **kwargs)
+ else:
+ return __highlight(*args, **kwargs)
+
+def __basic_highlight(lines, language='py'):
+ for line in lines:
+ yield cgi.escape(line.rstrip('\n'))
+
+def __highlight(lines, language='py'):
+ """A generator which will read lines from the given input stream, and yield them back
+ in syntax highlighted, HTML format. """
+
+ # trickier than this initially looks; the syntax highlighter might not necessarily
+ # have any more data for us, so it's necessary to use select() and write new data
+ # as it wants it, and read it back (buffer and yield lines) as possible.
+ #
+ # this assumes the 'highlight' command, although shouldn't take much work to have this
+ # happy with other highlighters.
+
+ fromchild_buf, tochild_buf = '', ''
+ to_run = [config.highlight_command, '--syntax', language, '-c', '/dev/null', '--quiet', '--xhtml', '--force', '--anchors', '--fragment']
+
+ process = popen2.Popen3(to_run, capturestderr=True)
+ map (set_nonblocking, [ process.fromchild,
+ process.tochild,
+ process.childerr ])
+ while True:
+ r_fds = [process.fromchild]
+ w_fds = []
+ if not process.tochild.closed:
+ w_fds.append(process.tochild)
+ r_stdin, r_stdout, r_stderr = select.select(r_fds, w_fds, [], None)
+ # debug ((r_stdin, r_stdout, r_stderr))
+ if not r_stdin and not r_stdout and not r_stderr:
+ break
+ if process.fromchild in r_stdin:
+ data = process.fromchild.read()
+ if data == "":
+ break
+ fromchild_buf += data
+
+ if process.tochild in r_stdout:
+ if tochild_buf == '':
+ try:
+ tochild_buf += lines.next()
+ except StopIteration:
+ tochild_buf = ''
+ if tochild_buf == '':
+ process.tochild.close()
+ else:
+ process.tochild.write(tochild_buf)
+ tochild_buf = ''
+
+ if fromchild_buf != '':
+ while True:
+ idx = fromchild_buf.find('\n')
+ if idx == -1:
+ break
+ yield fromchild_buf[:idx]
+ fromchild_buf = fromchild_buf[idx+1:]
+
+ # anything left over (presumably without a newline)
+ if fromchild_buf != '':
+ yield fromchild_buf
+
+ terminate_popen3(process)
+
+if __name__ == '__main__':
+ def l():
+ fd = open('syntax.py')
+ for line in fd:
+ yield line
+ for i in highlight(l()):
+ print i
============================================================
--- templates/branch.html 9b84a47baffe133624433db124e92d70a206bb6a
+++ templates/branch.html 9b84a47baffe133624433db124e92d70a206bb6a
@@ -0,0 +1,11 @@
+#extends base
+
+#def extramenu
+Branch $branch.name :
+Changes |
+Head revision
+#end def
+
+#def rssheaders
+
+#end def
============================================================
--- templates/branchchanges.html a8d275fa977d0714f41521f1409c3905ffe0236a
+++ templates/branchchanges.html a8d275fa977d0714f41521f1409c3905ffe0236a
@@ -0,0 +1,70 @@
+#extends branch
+
+#def body
+
+
+Changes $from_change to $to_change on this branch are displayed below, sorted in descending chronological order.
+
+
+
+
+#for $revision, $diffs, $ago, $author, $changelog, $shortlog, $when in $display_revs
+
+ Author:
+ $author
+
+
+ Changelog:
+
+#filter Filter
+$changelog
+#filter WebSafe
+
+
+
+ Date:
+ $when
+
+#end for
+
+
+
+
+
+#if $next_from and $next_to
+#filter Filter
+$link($branch, from_change=$next_from, to_change=$next_to).html("earlier changes")
+#filter WebSafe
+#else
+(no earlier changes)
+#end if
+
+
+
+#filter Filter
+$link($branch).html("recent changes")
+#filter WebSafe
+
+
+
+#if $previous_from and $previous_to
+#filter Filter
+$link($branch, from_change=$previous_from, to_change=$previous_to).html("later changes")
+#filter WebSafe
+#else
+(no later changes)
+#end if
+
+
+
+
+#end def
============================================================
--- templates/branchchangesrss.html 2be7274e1a3348545e8378190418bf00e4eab73b
+++ templates/branchchangesrss.html 2be7274e1a3348545e8378190418bf00e4eab73b
@@ -0,0 +1,23 @@
+
+
+ en-us
+
+ Changes to branch $branch.name
+ $dynamic_join('/')
+ Changes to branch $branch.name
+
+
+#for $revision, $diffs, $ago, $author, $changelog, $shortlog, $when in $display_revs
+-
+#filter Filter
+
$link($revision).uri()
+ $shortlog
+ $changelog
+#filter WebSafe
+ $author
+ $when +0000
+
+#end for
+
+
+
============================================================
--- templates/branchchoosehead.html e3233066d92401b9e6a0b6a8a20c2e3cc1aa383d
+++ templates/branchchoosehead.html e3233066d92401b9e6a0b6a8a20c2e3cc1aa383d
@@ -0,0 +1,27 @@
+#extends branch
+
+#def body
+
+
+There are multiple head revisions of the branch
+#filter Filter
+$link($branch).html()
+#filter WebSafe
+. You can access the method
+'$proxy_to' on each of these revisions by clicking on the links provided below. If you are
+attempting to access this method in a script, perhaps consider using this
+#filter Filter
+$anyhead
+#filter WebSafe
+which will always go directly to one of the head revisions.
+
+
+
+#for $head_link in $head_links
+ #filter Filter
+ $head_link
+ #filter WebSafe
+#end for
+
+
+#end def
============================================================
--- templates/help.html f8faec929ff6f5fd1722240f12a8b398c88fb603
+++ templates/help.html f8faec929ff6f5fd1722240f12a8b398c88fb603
@@ -0,0 +1,23 @@
+#extends base
+
+#def body
+
+ViewMTN is a web interface to the Monotone revision control
+system. These web pages provide various methods to access the data
+controlled within a particular Monotone database.
+
+
+
+To make full use of this web interface, it is recommended that you read
+the Monotone
+manual .
+
+
+
+Feature suggestions, bug reports and patches are welcome. Please go
+to the ViewMTN
+software page and follow the contact instructions there.
+
+
+#end def
============================================================
--- templates/revision.html e9eeb6212f211ce522b1db16156e7129e00993d1
+++ templates/revision.html e9eeb6212f211ce522b1db16156e7129e00993d1
@@ -0,0 +1,8 @@
+#extends base
+
+#def extramenu
+Revision $revision.abbrev() :
+Info |
+Browse Files |
+Download (tar)
+#end def
============================================================
--- templates/revisionbrowse.html a4175d3be3ff73d4e8d27667de546355fcece82d
+++ templates/revisionbrowse.html a4175d3be3ff73d4e8d27667de546355fcece82d
@@ -0,0 +1,57 @@
+#extends revision
+
+#def body
+
+#filter Filter
+
+Current directory:
+#for $l in $path_links
+$link($l).html()
+#end for
+
+
+These files are in a revision of
+#if len($branches) == 1
+branch
+#end if
+#if len($branches) > 1
+branches
+#end if
+#if $branches
+$branch_links
+#end if
+
+#filter WebSafe
+
+
+Name Age Author Last log entry
+#for $stanza_type, $this_path, $author, $ago, $content_mark, $shortlog, $mime_type in $entries
+
+
+
+
+
+ #filter Filter
+ $link($this_path).html()
+ #filter WebSafe
+
+
+ #if $content_mark
+ #filter Filter
+ $link($content_mark).html($ago, True)
+ #filter WebSafe
+ #end if
+
+
+ #filter Filter
+ $link($author).html()
+ #filter WebSafe
+
+
+ $shortlog
+
+
+#end for
+
+
+#end def
============================================================
--- templates/revisiondiff.html 06acbfdf78bebdc5ef8f0652278dfa829d49fbba
+++ templates/revisiondiff.html 06acbfdf78bebdc5ef8f0652278dfa829d49fbba
@@ -0,0 +1,29 @@
+#extends revision
+
+#def body
+
+
+#filter Filter
+
+The unified diff between revisions $link($revision_from).html() and $link($revision_to).html() is displayed below.
+
+#filter WebSafe
+
+#if $files
+
+This diff has been restricted to the following files:
+ #for $fname in $files
+ '$fname'
+ #end for
+
+#end if
+
+
+#filter Filter
+#for $line in $diff
+$line
+#end for
+#filter WebSafe
+
+
+#end def
============================================================
--- templates/revisionfile.html 652180e381d9e181020c3a4a52f5bc85cef8397d
+++ templates/revisionfile.html 652180e381d9e181020c3a4a52f5bc85cef8397d
@@ -0,0 +1,15 @@
+#extends revision
+
+#def body
+
+
+Below is the file '$filename.name' from this revision. You can also
+#filter Filter
+$link($filename, for_download=True).html(override_description="download the file").
+#filter WebSafe
+
+
+#block filecontents
+#end block
+
+#end def
============================================================
--- templates/revisionfilebin.html ed073e34de5e16ad7ed0d14ce01bff80b0d78970
+++ templates/revisionfilebin.html ed073e34de5e16ad7ed0d14ce01bff80b0d78970
@@ -0,0 +1,10 @@
+#extends revisionfile
+
+#def filecontents
+
+Unfortunately, this ViewMTN has determined that this file (with
+MIME type $mimetype) is not suitable for display inside the web
+browser. If you feel this file could have been better displayed
+please inform the author.
+
+#end def
============================================================
--- templates/revisionfileimg.html cd246dd75333b2ced5d8dd9216e0d50a16769dfd
+++ templates/revisionfileimg.html cd246dd75333b2ced5d8dd9216e0d50a16769dfd
@@ -0,0 +1,5 @@
+#extends revisionfile
+
+#def filecontents
+
+#end def
============================================================
--- templates/revisionfileobj.html aecf0edca364646c23f9fecfb764159a73b4d7d1
+++ templates/revisionfileobj.html aecf0edca364646c23f9fecfb764159a73b4d7d1
@@ -0,0 +1,11 @@
+#extends revisionfile
+
+#def filecontents
+
+Your browser doesn't support viewing content of type $mimetype . Use the download
+link from above instead.
+
+#end def
============================================================
--- templates/revisionfiletxt.html 8abafa4c4189b5c596ac59e6061d0ad116909393
+++ templates/revisionfiletxt.html 8abafa4c4189b5c596ac59e6061d0ad116909393
@@ -0,0 +1,12 @@
+#extends revisionfile
+
+#def filecontents
+
+#filter Filter
+#for $line in $contents
+$line
+#end for
+#filter WebSafe
+
+#end def
+
============================================================
--- templates/revisioninfo.html 05af137b031ca0fe6ff4d5200d3958a18d38de60
+++ templates/revisioninfo.html 05af137b031ca0fe6ff4d5200d3958a18d38de60
@@ -0,0 +1,54 @@
+#extends revision
+
+#def body
+
+
+
Certificates
+
+
+#for cert in $certs
+
+
+ #filter Filter
+ $cert['name']
+ #filter WebSafe
+
+
+ #filter Filter
+ $cert['value']
+ #filter WebSafe
+
+
+#end for
+
+
+
Revision Details
+
+
+#for grouping, stanzagroup in $revisions
+
+
+ #filter Filter
+ $grouping
+ #filter WebSafe
+
+
+ #filter Filter
+ #for value in $stanzagroup
+ $value
+ #end for
+ #filter WebSafe
+
+
+#end for
+
+
+
+#filter Filter
+$imagemap
+#filter WebSafe
+
+
+
+
+#end def
============================================================
--- templates/tags.html e37d06408dc812404695b1ac441426fa5c39e051
+++ templates/tags.html e37d06408dc812404695b1ac441426fa5c39e051
@@ -0,0 +1,36 @@
+#extends base
+
+#def body
+
+A tag marks a particular revision that is in some way significant.
+A common use of tags is to mark public release of a piece of software.
+To view a particular tag, select it from the list below.
+
+
+
+Tag Signed by Branches Age
+#for tag in $tags
+
+
+ #filter Filter
+ $link($tag).html()
+ #filter WebSafe
+
+
+ $tag.author
+
+
+ #filter Filter
+ #for branch in $tag.branches
+ $link($branch).html()
+ #end for
+ #filter WebSafe
+
+
+ $revision_ago($tag.revision)
+
+
+#end for
+
+
+#end def
============================================================
--- TODO 6372d06a8696794b11e1f5dece588de4d1057896
+++ TODO 9caf49f2ed434f3943a89b627d974c02f597ed49
@@ -1,7 +1,18 @@
+NEW VERSION
+
+ * Fix the RSS date fields
+ * Highlight -> content_type mapping (works at the moment, somehow)
+ * JSON
+ * Unit tests
+ * Read-only WebDAV support
+ * Put copyright notices on the files, mostly so I can keep track
+ of where they end up.
+
BUGS:
- * \n in title of the ancestry graph
+ * HEAD just does GET; we should really do HEAD properly. Also, we
+ should start issuing eTags
TODO:
@@ -20,16 +31,6 @@ TODO:
which updates with the commands that have been run - useful for debugging,
also useful for beginners to see how to do stuff.
- * Use monotone automate graph to do the ancestry graphing, and show some
- future information. Perhaps cache based on the mtime of the db viewmtn
- is looking at? Might be good enough.
-
- Show dotted lines for propogates (with that information on the arc)
-
- Show lines leading off from the boxes at either end of the graph (if
- applicable) to give context that the image being shown is part of a
- large overall graph.
-
* Show information when mousing over long hex strings.
* Magically make http:// ftp:// etc links inside files clickable
@@ -39,20 +40,10 @@ TODO:
explain what's happened. For bonus points, index into the diff so you
can click on a file that changed and jump to that section in the diff.
- * Improve the file listing; use data from the new rosters branch
- to get a better idea of when a given file was last touched.
-
* When viewing a file, give a list of revisions in which that file
was recently changed. Include in the list a link to diff with each
revision.
- * generally clean up table formatting code; things like
- list_of_branches()
- list_of_tags()
- revision_certs()
- revision_details()
- use a generic method to output the tables
-
* from [mrb]
support for multiple databases (perhaps some sort of dropdown to say which
database you want to look in) - perhaps also make the branches page show the
@@ -80,10 +71,3 @@ AJAX ideas:
* we need some concept of selecting two points revisions for diffs or other
comparison. This is the main strength ViewCVS seems to have over us.
-LONG TERM:
-
- * provide some option for people without apache2 / mod_python to run the
- thing; even if it's running the program from a standalone python webserver.
- Would settle for a solution that required a cgi capable webserver rather than
- specifically mod_python (and thus apache2) while still supporting mod_python
- acceleration if present.
============================================================
--- common.py f77fd516022cfc46ebfc2070554f08eb36e1cee3
+++ common.py 1462ca963d4b3959019bb0ac26a5311f6f2bd406
@@ -1,82 +1,50 @@ import datetime
import datetime
-import urllib
-import pydoc
import time
+import fcntl
+import os
+import signal
+from web import debug
+import traceback
-escape_function = pydoc.HTMLRepr().escape
-
-def type_wrapper(e, x):
- if x == None:
- return ""
- elif type(x) == type([]):
- return ' '.join(map(e, x))
- else:
- return e(x)
-
def parse_timecert(value):
return apply(datetime.datetime, time.strptime(value, "%Y-%m-%dT%H:%M:%S")[:6])
-def get_branch_links(mt, branches):
- if len(branches) > 1:
- branch_links = "branches "
- else:
- branch_links = "branch "
- links = []
- for branch in branches:
- links.append(link(mt, "branch", branch))
- return branch_links + ', '.join(links)
+def set_nonblocking(fd):
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY)
-def extract_cert_from_certs(certs, certname, as_list=False):
- rv = []
- for cert in certs:
- name, value = None, None
- for k, v in cert:
- if k == "name": name = v
- elif k == "value": value = v
- if name == None or value == None: continue
- if name == certname:
- if not as_list:
- return value
- else:
- rv.append(value)
- return rv
+def terminate_popen3(process):
+ debug("[%s] stopping process: %s" % (os.getpid(), process.pid))
+ try:
+ process.tochild.close()
+ process.fromchild.close()
+ process.childerr.close()
+ if process.poll() == -1:
+ # the process is still running, so kill it.
+ os.kill(process.pid, signal.SIGKILL)
+ process.wait()
+ except:
+ debug("%s failed_to_stop %s (%s)" % (os.getpid(), process.pid, traceback.format_exc()))
-def determine_date(certs):
- dateval = extract_cert_from_certs(certs, "date")
- if dateval == None:
- return None
- else:
- return parse_timecert(dateval)
-
-def quicklog(value):
- hq = html_escape()
- rv = hq(value.strip().split('\n')[0])
- if rv.startswith('*'):
- rv = rv[1:].strip()
- return rv
-
-def ago_string(event, now):
+def ago(event):
def plural(v, singular, plural):
- if v == 1:
- return "%d %s" % (v, singular)
- else:
- return "%d %s" % (v, plural)
+ if v == 1:
+ return "%d %s" % (v, singular)
+ else:
+ return "%d %s" % (v, plural)
now = datetime.datetime.utcnow()
ago = now - event
if ago.days > 0:
- rv = "%s, %s" % (plural(ago.days, "day", "days"),
- plural(ago.seconds / 3600, "hour", "hours"))
+ rv = "%s" % (plural(ago.days, "day", "days"))
elif ago.seconds > 3600:
hours = ago.seconds / 3600
minutes = (ago.seconds - (hours * 3600)) / 60
- rv = "%s, %s" % (plural(hours, "hour", "hours"),
- plural(minutes, "minute", "minutes"))
+ rv = "%s" % (plural(hours, "hour", "hours"))
else:
minutes = ago.seconds / 60
seconds = (ago.seconds - (minutes * 60))
- rv = "%s, %s" % (plural(minutes, "minute", "minutes"),
- plural(seconds, "second", "seconds"))
+ rv = "%s" % (plural(minutes, "minute", "minutes"))
return rv
def link(mt, link_type, link_to, description = None, no_quote = False):
============================================================
--- config.py.example 7627e154e30ee6e88e547312876aa5cca8bf7c7e
+++ config.py.example a484f47e78d3b8aa4fcc875b44f1039220fb2837
@@ -9,59 +9,69 @@
# If config changes are not picked up, reloading
# the web server should solve the issue.
#
-# If you want to run multiple viewmtn installs from
-# a single apache server, you might want to look at
-# giving them seperate python interpreter instances,
-# ie set PythonInterpreter viewmtn1, viewmtn2 etc
-# in .htaccess.
import sys
-# the base URL of this install
-base_url = 'http://localhost/~grahame/viewmtn/'
+dynamic_uri_path = 'http://glamdring.local:8080/'
+static_uri_path = 'http://glamdring.local/~grahame/viewmtn/static/'
-# the path to the 'monotone' binary
-monotone = '/opt/monotone/bin/monotone'
+# the path to the 'mtn' binary
+monotone = '/opt/mtn/bin/mtn'
# the monotone database to be shared out
# obviously, everything in this database might
# become public if something goes wrong; probably
# a good idea not to leave your private key in it
-dbfile = '/path/to/monotone.db'
+dbfile = '/Users/grahame/mtn/db/angrygoats.db'
# where to find GNOME icons (used in manifest listing)
-gnome_mimetype_icon_path = '/path/to/share/icons/gnome/'
+# set to None to disableicon loading
+gnome_mimetype_icon_path = '/Users/grahame/mtn/viewmtn/mimetypes/'
# and where they are on the web
gnome_mimetype_uri = 'mimetypes/'
-# where to find GNU enscript
-enscript_path = '/usr/bin/enscript'
+# highlight from http://andre-simon.de/
+# if you don't have this available, just comment
+# the "highlight_command" line out
+highlight_command = '/opt/local/bin/highlight'
graphopts = {
- # a directory (must be writable by the web user)
- # in which viewmtn can output graph files
- # (you should set up a cronjob to delete old ones
- # periodically)
- 'directory' : '/path/to/graph/directory',
+ # a directory (must be writable by the web user)
+ # in which viewmtn can output graph files
+ # (you should set up a cronjob to delete old ones
+ # periodically)
+ 'directory' : '/Users/grahame/mtn/viewmtn/graph/',
- # a URL, relative or absolute, at which the files
- # in the 'graphdir' directory can be found. Should
- # end in a '/' character
- 'uri' : 'graph/',
+ # a URL, relative or absolute, at which the files
+ # in the 'graphdir' directory can be found. Should
+ # end in a '/' character
+ 'uri' : 'graph/',
- # the path to the 'dot' program
- 'dot' : '/usr/bin/dot',
+ # the path to the 'dot' program
+ 'dot' : '/opt/local/bin/dot',
- # options to use for nodes in the dot input file
- # we generate.
- 'nodeopts' : { 'fontname' : 'Windsor',
- 'fontsize' : '8',
- 'shape' : 'box',
- 'height' : '0.3',
- 'spline' : 'true',
- 'style' : 'filled',
- 'fillcolor' : '#dddddd' }
+ # options to use for nodes in the dot input file
+ # we generate.
+ 'nodeopts' : { 'fontname' : 'Monaco',
+ 'fontsize' : '8',
+ 'shape' : 'box',
+ 'height' : '0.3',
+ 'spline' : 'true',
+ 'style' : 'filled',
+ 'fillcolor' : '#dddddd' }
}
+# Icon Theme to use for icons; 'gnome' is a safe value,
+# as is 'hicolor'. Note that icon_size must be a string,
+# not an integer
+icon_theme = 'gnome'
+icon_size = '16'
+# for tests/
+# don't worry about these unless you are going to run
+# the tests
+test_branch = "net.angrygoats.viewmtn"
+#test_revision = "53b7a6866f0f7268a8eb721e8d74688de8567fb8"
+test_revision = "ea14ea3aadb3a02ffe5041e0a98db15306cbcd81"
+
============================================================
--- version.py a071b2192e2092ec222ab0d59ef95efd1c9c81e6
+++ release.py 9606208d767e8ed4999ca37fa5e20d13f3fa7be5
@@ -1,3 +1,12 @@
+version='0.06beta'
+authors='''Authors:
+Grahame Bowland
+Contributors:
+Matt Johnston
+Nathaniel Smith
+Bruce Stephens
+Lapo Luchini
+David Reiss
+
+'''
-# the latest release; make sure to update this (note for Grahame)
-release = "0.05"
============================================================
--- builtpython.sh c1d18a362622ec209ea6818957a5778d5374de45
+++ release.sh 65fe70e241d026a9e09530245362cb09287b6608
@@ -1,16 +1,11 @@
#!/bin/sh
-# generate the list of enscript formatting options
-LANGS=enscriptlangs.py
-echo -n 'enscript_langs = [' > "$LANGS"
-for i in `enscript --help-highlight | grep Name | awk {'print $2'}`; do
- echo -n "'$i', " >> "$LANGS"
-done; echo ']' >> "$LANGS"
-
# generate the help file data
-AUTHORS=authors.py
-echo -n "authors='''" > "$AUTHORS"
-cat AUTHORS >> "$AUTHORS"
-echo "'''" >> "$AUTHORS"
+OUT="release.py"
+RELEASE="0.06beta"
+echo -n > "$OUT"
+echo "version='$RELEASE'" > "$OUT"
+echo -n "authors='''" >> "$OUT"
+cat AUTHORS >> "$OUT"
+echo "'''" >> "$OUT"
-
============================================================
--- viewmtn.css 6efcadac0d56fb3d77a22786f477ba621f4af33d
+++ static/viewmtn.css 8981409323f375efedb9599c206ac0b34bdad739
@@ -47,11 +47,11 @@ TABLE.pretty TH {
padding-right: 0.5em;
}
-TR.odd {
+TR.even {
background-color: #eeeeee;
}
-TR.even {
+TR.odd {
background-color: #ffffff;
}
@@ -91,3 +91,10 @@ DIV#popupBox {
padding: 2px;
z-index: 10;
}
+
+PRE.code {
+ border-left-style: solid;
+ border-left-width: 3px;
+ border-left-color: #A0A0A0;
+ padding-left: 3px;
+}
============================================================
--- templates/about.html 6b1c55564ae50e2de59d5eda418bfad86f0b596d
+++ templates/about.html fea245cb17ab46a7766e03293800361db0b325b2
@@ -5,18 +5,18 @@
Authors and Contributors
- Grahame Bowland - address@hidden
- Matt Johnston - address@hidden
- Nathaniel Smith - address@hidden
- Bruce Stephens - address@hidden
- Lapo Luchini - address@hidden
- David Reiss - address@hidden
+ Grahame Bowland (grahame at angrygoats.net)
+ Matt Johnston (matt at ucc.asn.au)
+ Nathaniel Smith (njs at pobox.com)
+ Bruce Stephens (monotone at cenderis.demon.co.uk)
+ Lapo Luchini (lapo at lapo.it)
+ David Reiss (davidn at gmail.com)
Licensing
-Copyright (C) 2005 Grahame Bowland
+Copyright (C) 2005-2006 Grahame Bowland
@@ -42,26 +42,12 @@ Foundation, Inc., 59 Temple Place, Suite
Dependencies
-ViewMTN is written in Python and
-runs under mod_python .
+ViewMTN is written in Python and runs under web.py . Code highlighting via Highlight .
+Graphing via GraphViz . Graph colour generation algorithm
+from monotone-viz with modifications from Matt Johnston. AJAX funtionality uses the MochiKit Javascript library.
-
-Code highlighting via
-GNU Enscript .
-
-
-Graphing via GraphViz .
-
-
-
-Graph colour generation algorithm from monotone-viz with modifications from Matt Johnston.
-
-
-
-AJAX funtionality uses the MochiKit Javascript library.
-
-
-
#end def
============================================================
--- templates/base.html 941ad9f0b2cc72c729f78185d140cce2f5088e29
+++ templates/base.html 96ce364d2dfd62b29e6557bebfd3e5f9a6c87b46
@@ -1,13 +1,16 @@
+
#block extraheaders
#end block
+#block rssheaders
+#end block
@@ -18,6 +21,9 @@
Tags |
Help |
About
+
+#block extramenu
+#end block
#block body
@@ -28,7 +34,7 @@ installCallbacks();
============================================================
--- templates/index.html e0ae8e2a7cc89a8ff8d012742104ded01f6cf968
+++ templates/index.html 08145fdb8457c0f05ead0cdec4717e0f7f0de864
@@ -9,6 +9,21 @@ Select one of the branches and you will
Select one of the branches and you will be shown a list of recent changes that have occurred within it.
-If you are looking for a particular revision (for example, a release) the list of tags might be useful.
+If you are looking for a particular revision (for example, a release) the list of tags
+might be useful.
+
+
+Branch
+#for branch in $branches
+
+
+ #filter Filter
+ $link($branch).html()
+ #filter WebSafe
+
+
+#end for
+
+
#end def
============================================================
--- viewmtn.py b33f71afb2e598fadb81114ed5a569af368d390b
+++ viewmtn.py 1dc5caa196fe72d70fa67baaad7ed79f3a430868
@@ -1,10 +1,31 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2.4
+import os
+import cgi
+import mtn
+import sha
+import sys
import web
+import struct
+import string
+import rfc822
import config
-
+import common
+import urllib
import urlparse
+import syntax
+import tarfile
+import tempfile
+import datetime
+import cStringIO
+from colorsys import hls_to_rgb
+from fdo import sharedmimeinfo, icontheme
+import release
+hq = cgi.escape
+import web
+debug = web.debug
+
# /about.psp -> /about
# /branch.psp -> /branch/{branch}/
@@ -27,39 +48,980 @@ import urlparse
# /getjson.py -> /json[...] (private)
+dynamic_join = lambda path: urlparse.urljoin(config.dynamic_uri_path, path)
+static_join = lambda path: urlparse.urljoin(config.static_uri_path, path)
+
+def quicklog(changelog, max_size=None):
+ interesting_line = None
+ for line in changelog:
+ line = line.strip()
+ if line:
+ interesting_line = line
+ break
+ if not interesting_line:
+ return ""
+ if interesting_line.startswith('*'):
+ interesting_line = interesting_line[1:].strip()
+ if max_size and len(interesting_line) > max_size:
+ interesting_line = interesting_line[:max_size]
+ r_wspc = interesting_line.rfind(' ')
+ if r_wspc <> -1:
+ interesting_line = interesting_line[:r_wspc]
+ interesting_line += '..'
+ return interesting_line
+
+def timecert(certs):
+ revdate = None
+ for cert in certs:
+ if cert[4] == 'name' and cert[5] == 'date':
+ revdate = common.parse_timecert(cert[7])
+ return revdate
+
+def nbhq(s):
+ return ' '.join([hq(t) for t in s.split(' ')])
+
+def normalise_changelog(changelog):
+ changelog = map(hq, changelog.split('\n'))
+ if changelog and changelog[-1] == '':
+ changelog = changelog[:-1]
+ return changelog
+
+class Link:
+ def __init__(self, description=None, link_type=None, **kwargs):
+ self.absolute_uri = None
+ self.relative_uri = None
+ self.description = description
+ def uri(self):
+ return dynamic_join(self.relative_uri)
+ def html(self, override_description=None, force_nbsp=False):
+ if override_description:
+ if force_nbsp:
+ d = nbhq(override_description)
+ else:
+ d = hq(override_description)
+ else:
+ d = self.description
+ if self.relative_uri:
+ uri = dynamic_join(self.relative_uri)
+ elif self.absolute_uri:
+ uri = self.absolute_uri
+ else:
+ return self.description
+ return '%s ' % (uri, d)
+
+class AuthorLink(Link):
+ def __init__(self, author, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ name, email = rfc822.parseaddr(author)
+ self.description = author
+ if email:
+ self.absolute_uri = "mailto:%s" % urllib.quote(email)
+ if name:
+ self.description = hq(name)
+
+class RevisionLink(Link):
+ def __init__(self, revision, **kwargs):
+ link_type = kwargs.get("link_type")
+ if link_type == "browse":
+ subpage = "browse"
+ else:
+ subpage = "info"
+ Link.__init__(*(self, ), **kwargs)
+ self.relative_uri = 'revision/%s/%s' % (subpage, revision)
+ self.description = revision.abbrev()
+
+class TagLink(Link):
+ def __init__(self, tag, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ self.relative_uri = 'revision/info/%s' % (tag.revision)
+ self.description = tag.name
+
+class BranchLink(Link):
+ def __init__(self, branch, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ from_change, to_change = kwargs.get('from_change'), kwargs.get('to_change')
+ if from_change and to_change:
+ self.relative_uri = 'branch/changes/%s/from/%d/to/%d' % (urllib.quote(branch.name), from_change, to_change)
+ else:
+ self.relative_uri = 'branch/changes/' + urllib.quote(branch.name)
+ self.description = hq(branch.name)
+
+class DiffLink(Link):
+ def __init__(self, diff, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ self.relative_uri = 'revision/diff/' + diff.from_rev + '/with/' + diff.to_rev
+ if diff.fname:
+ self.relative_uri += '/'+urllib.quote(diff.fname)
+ self.description = "diff"
+
+class DirLink(Link):
+ def __init__(self, file, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ # handle the root directory
+ if file.name == '/':
+ fn = ''
+ else:
+ fn = file.name
+ self.relative_uri = 'revision/browse/' + file.in_revision + '/' + urllib.quote(fn)
+ self.description = hq(file.name)
+
+class FileLink(Link):
+ def __init__(self, file, **kwargs):
+ Link.__init__(*(self, ), **kwargs)
+ if kwargs.has_key('for_download'):
+ access_method = 'downloadfile'
+ else:
+ access_method = 'file'
+ self.relative_uri = 'revision/' + access_method + '/' + file.in_revision + '/' + urllib.quote(file.name)
+ self.description = hq(file.name)
+
+class Diff:
+ def __init__(self, from_rev, to_rev, fname=None):
+ self.obj_type = 'diff'
+ self.fname = fname
+ self.from_rev = from_rev
+ self.to_rev = to_rev
+
+def prettify(s):
+ return ' '.join([hq(x[0].upper() + x[1:]) for x in s.replace("_", "").split(" ")])
+
+def certs_for_template(cert_gen):
+ for cert in cert_gen:
+ if cert[0] == 'key' and len(cert) != 10:
+ raise Exception("Not a correctly formatted certificate: %s" % cert)
+ if cert[3] != 'ok':
+ raise Exception("Certificate failed check.")
+
+ key = cert[1]
+ name = cert[5]
+ value = cert[7]
+ if name == "branch":
+ value = link(mtn.Branch(value)).html()
+ else:
+ value = ' '.join(map(hq, value.split('\n')))
+
+ yield { 'key' : key,
+ 'name' : prettify(name),
+ 'value' : value }
+
+def revisions_for_template(revision, rev_gen):
+ old_revisions = []
+ stanzas = []
+ grouping = None
+ for stanza in rev_gen:
+ stanza_type = stanza[0]
+ description, value = prettify(stanza_type), None
+
+ if grouping == None:
+ grouping = description
+ if description != grouping:
+ if len(stanzas) > 0:
+ yield grouping, stanzas
+ grouping, stanzas = description, []
+
+ if stanza_type == "format_version" or \
+ stanza_type == "new_manifest":
+ continue
+ elif stanza_type == "patch":
+ fname, from_id, to_id = stanza[1], stanza[3], stanza[5]
+ # if from_id is null, this is a new file
+ # since we're showing that information under "Add", so
+ # skip it here
+ if not from_id:
+ continue
+ diff_links = ','.join([link(Diff(old_revision, revision, fname)).html() for old_revision in old_revisions])
+ value = "Patch file %s (%s)" % (link(mtn.File(fname, revision)).html(), diff_links)
+ elif stanza_type == "old_revision":
+ old_revision = mtn.Revision(stanza[1])
+ old_revisions.append(old_revision)
+ value = "Old revision is: %s (%s)" % (link(old_revision).html(), link(Diff(old_revision, revision)).html())
+ elif stanza_type == "add_file":
+ fname = stanza[1]
+ value = "Add file: %s" % (link(mtn.File(fname, revision)).html())
+ elif stanza_type == "add_dir":
+ dname = stanza[1]
+ value = "Add directory: %s" % (hq(dname))
+ elif stanza_type == "delete":
+ fname = stanza[1]
+ value = "Delete: %s" % (hq(fname))
+ elif stanza_type == "set":
+ fname, attr, value = stanza[1], stanza[3], stanza[5]
+ value = "Set attribute '%s' to '%s' upon %s" % (hq(attr), hq(value), link(mtn.File(fname, revision)).html())
+ elif stanza_type == "rename":
+ oldname, newname = stanza[1], stanza[3]
+ value = "Rename %s to %s" % (hq(oldname), link(mtn.File(newname, revision)).html())
+ else:
+ value = "(this stanza type is not explicitly rendered; please report this.)\n%s" % hq(str(stanza))
+
+ if description != None:
+ stanzas.append(value)
+
+ if len(stanzas) > 0:
+ yield grouping, stanzas
+
+type_to_link_class = {
+ 'author' : AuthorLink,
+ 'branch' : BranchLink,
+ 'diff' : DiffLink,
+ 'dir' : DirLink,
+ 'file' : FileLink,
+ 'revision' : RevisionLink,
+ 'tag' : TagLink,
+}
+
+def link(obj, link_type=None, **kwargs):
+ link_class = type_to_link_class.get(obj.obj_type)
+ if not link_class:
+ raise LinkException("Unable to link to objects of type: '%s'" % (obj.obj_type))
+ # ugh
+ if link_type:
+ kwargs['link_type'] = link_type
+ return link_class(obj, **kwargs)
+
class Renderer:
def __init__(self):
- # any templates that can be inherited from, should be added to the list here
- templates = [ ('base.html', 'base'), ]
- for template, mod_name in templates:
- web.render(template, None, True, mod_name)
- self.terms = {
- 'dynamic_uri_path' : config.dynamic_uri_path,
- 'dynamic_join' : lambda path: urlparse.urljoin(config.dynamic_uri_path, path),
- 'static_uri_path' : config.static_uri_path,
- 'static_join' : lambda path: urlparse.urljoin(config.static_uri_path, path),
- }
+ # any templates that can be inherited from, should be added to the list here
+ self.templates = [ ('base.html', 'base'),
+ ('revision.html', 'revision'),
+ ('branch.html', 'branch'),
+ ('revisionfile.html', 'revisionfile') ]
+ self._templates_loaded = False
+
+ # these variables will be available to any template
+ self.terms = {
+ 'context' : web.context, # fugly
+ 'dynamic_uri_path' : config.dynamic_uri_path,
+ 'dynamic_join' : dynamic_join,
+ 'static_uri_path' : config.static_uri_path,
+ 'static_join' : static_join,
+ 'link' : link,
+ 'version' : release.version,
+ }
+
+ def load_templates(self):
+ if self._templates_loaded: return
+ for template, mod_name in self.templates:
+ web.render(template, None, True, mod_name)
+ self._templates_loaded = True
+
def render(self, template, **kwargs):
- terms = self.terms.copy()
- terms.update(kwargs)
- web.render(template, terms)
+ self.load_templates()
+ terms = self.terms.copy()
+ terms.update(kwargs)
+ web.render(template, terms)
renderer = Renderer()
+ops = mtn.Operations([config.monotone, config.dbfile])
+mimehelp = sharedmimeinfo.LookupHelper()
+mimeicon = icontheme.MimeIcon(icontheme.IconTheme(config.icon_theme), config.icon_size)
class Index:
def GET(self):
- renderer.render('index.html', page_title="Branches")
+ renderer.render('index.html', page_title="Branches", branches=ops.branches())
class About:
def GET(self):
- renderer.render('about.html', page_title="About")
- page_title = "About"
+ renderer.render('about.html', page_title="About")
-id_re = r'[A-Za-z0-9]{40}'
+class Tags:
+ def GET(self):
+ # otherwise we couldn't use automate again..
+ tags = map(None, ops.tags())
+ tags.sort(lambda t1, t2: cmp(t1.name, t2.name))
+ def revision_ago(rev):
+ rv = ""
+ for cert in ops.certs(rev):
+ if cert[4] == 'name' and cert[5] == 'date':
+ revdate = common.parse_timecert(cert[7])
+ rv = common.ago(revdate)
+ return rv
+ renderer.render('tags.html', page_title="Tags", tags=tags, revision_ago=revision_ago)
+
+class Help:
+ def GET(self):
+ renderer.render('help.html', page_title="Help")
+
+class BranchChanges:
+ def get_last_changes(self, branch, heads, from_change, to_change):
+ revs = heads
+ if len(revs) == 0:
+ raise Exception("get_last_changes() unable to find somewhere to start - probably a non-existent branch?")
+ to_parent = revs+[] # copy
+ count = to_change
+
+ def on_our_branch(r):
+ rv = False
+ for cert in ops.certs(r):
+ if cert[4] == 'name' and cert[5] == 'branch':
+ if cert[7] == branch.name:
+ rv = True
+ return rv
+
+ while len(revs) < count:
+ new_to_parent = []
+ for rev in to_parent:
+ # we must be cautious; we only want to look at parents on our branch!
+ parents = map(None, ops.parents(rev))
+ parents = filter(on_our_branch, parents)
+ new_to_parent += parents
+ if len(new_to_parent) == 0:
+ # out of revisions...
+ break
+ to_parent = new_to_parent
+ revs += new_to_parent
+# toposort seems pretty darn slow; let's avoid this one..
+# revs = map(None, ops.toposort(revs))[:count]
+ certs_for_revs = []
+ for rev in revs:
+ certs_for_revs.append((rev, map(None, ops.certs(rev))))
+ def cd(certs):
+ for cert in certs:
+ if cert[4] == 'name' and cert[5] == 'date':
+ return common.parse_timecert(cert[7])
+ return None
+ certs_for_revs.sort(lambda b, a: cmp(cd(a[1]), cd(b[1])))
+ return certs_for_revs[from_change:to_change], new_to_parent
+
+ def GET(self, branch, from_change, to_change, template_name):
+ def for_template(revs):
+ rv = []
+ for rev, certs in revs:
+ rev_branch = ""
+ revision, diffs, ago, author, changelog, shortlog, when = mtn.Revision(rev), [], "", mtn.Author(""), "", "", ""
+ for cert in certs:
+ if cert[4] != 'name':
+ continue
+ if cert[5] == "branch":
+ rev_branch = cert[7]
+ elif cert[5] == 'date':
+ when = cert[7]
+ revdate = common.parse_timecert(when)
+ ago = common.ago(revdate)
+ elif cert[5] == 'author':
+ author = mtn.Author(cert[7])
+ elif cert[5] == 'changelog':
+ changelog = normalise_changelog(cert[7]) # NB: this HTML escapes!
+ shortlog = quicklog(changelog) # so this is also HTML escaped.
+ if rev_branch != branch.name:
+ # yikes, fallen down a well
+ continue
+ for stanza in ops.get_revision(rev):
+ if stanza and stanza[0] == "old_revision":
+ old_revision = stanza[1]
+ diffs.append(Diff(mtn.Revision(old_revision), revision))
+ if diffs:
+ diffs = '| ' + ', '.join([link(d).html('diff') for d in diffs])
+ else:
+ diffs = ''
+ rv.append((revision, diffs, ago, mtn.Author(author), ' \n'.join(changelog), shortlog, when))
+ return rv
+
+ branch = mtn.Branch(branch)
+ heads = [t for t in ops.heads(branch.name)]
+ if not heads:
+ return web.notfound()
+ per_page = 10
+ if from_change:
+ from_change = int(from_change)
+ else:
+ from_change = 0
+ if to_change:
+ to_change = int(to_change)
+ else:
+ to_change = per_page
+ changed, new_starting_point = self.get_last_changes(branch, heads, from_change, to_change)
+ # next and previous 'from' and 'to' indexes
+ if len(changed) == to_change - from_change:
+ next_from, next_to = to_change, to_change + per_page
+ else:
+ next_from, next_to = None, None
+ if from_change > 0:
+ previous_from = from_change - per_page
+ if previous_from < 0: previous_from = 0
+ previous_to = previous_from + per_page
+ else:
+ previous_from, previous_to = None, None
+
+ renderer.render(template_name,
+ page_title="Branch %s" % branch.name,
+ branch=branch,
+ from_change=from_change,
+ to_change=to_change,
+ previous_from=previous_from,
+ previous_to=previous_to,
+ next_from=next_from,
+ next_to=next_to,
+ display_revs=for_template(changed))
+
+class HTMLBranchChanges(BranchChanges):
+ def GET(self, branch, from_change, to_change):
+ BranchChanges.GET(self, branch, from_change, to_change, "branchchanges.html")
+
+class RSSBranchChanges(BranchChanges):
+ def GET(self, branch, from_change, to_change):
+ BranchChanges.GET(self, branch, from_change, to_change, "branchchangesrss.html")
+
+class RevisionPage(object):
+ def get_fileid(self, revision, filename):
+ rv = None
+ for stanza in ops.get_manifest_of(revision):
+ if stanza[0] != 'file':
+ continue
+ if stanza[1] == filename:
+ rv = stanza[3]
+ return rv
+ def exists(self, revision):
+ try:
+ certs = [t for t in ops.certs(revision)]
+ return True
+ except mtn.MonotoneException:
+ return False
+ def branches_for_rev(self, revisions_val):
+ rv = []
+ for stanza in ops.certs(revisions_val):
+ if stanza[4] == 'name' and stanza[5] == 'branch':
+ rv.append(stanza[7])
+ return rv
+
+class RevisionInfo(RevisionPage):
+ def GET(self, revision):
+ revision = mtn.Revision(revision)
+ if not self.exists(revision):
+ return web.notfound()
+ certs = ops.certs(revision)
+ revisions = ops.get_revision(revision)
+ output_png, output_imagemap = ancestry_graph(revision)
+ if os.access(output_imagemap, os.R_OK):
+ imagemap = open(output_imagemap).read().replace('\\n', ' by ')
+ imageuri = dynamic_join('/revision/graph/' + revision)
+ else:
+ imagemap = imageuri = None
+ renderer.render('revisioninfo.html',
+ page_title="Revision %s" % revision.abbrev(),
+ revision=revision,
+ certs=certs_for_template(certs),
+ imagemap=imagemap,
+ imageuri=imageuri,
+ revisions=revisions_for_template(revision, revisions))
+
+class RevisionDiff(RevisionPage):
+ def GET(self, revision_from, revision_to, filename=None):
+ revision_from = mtn.Revision(revision_from)
+ revision_to = mtn.Revision(revision_to)
+ if not self.exists(revision_from):
+ return web.notfound()
+ if not self.exists(revision_to):
+ return web.notfound()
+ if filename != None:
+ files = [filename]
+ else:
+ files = []
+ diff = ops.diff(revision_from, revision_to, files)
+ renderer.render('revisiondiff.html',
+ page_title="Diff from %s to %s" % (revision_from.abbrev(), revision_to.abbrev()),
+ revision=revision_from,
+ revision_from=revision_from,
+ revision_to=revision_to,
+ diff=syntax.highlight(diff, 'diff'),
+ files=files)
+
+class RevisionFile(RevisionPage):
+ def GET(self, revision, filename):
+ revision = mtn.Revision(revision)
+ if not self.exists(revision):
+ return web.notfound()
+ language = filename.rsplit('.', 1)[-1]
+ fileid = RevisionPage.get_fileid(self, revision, filename)
+ if not fileid:
+ return web.notfound()
+ contents = ops.get_file(fileid)
+ mimetype = mimehelp.lookup(filename, '')
+ mime_to_template = {
+ 'image/jpeg' : 'revisionfileimg.html',
+ 'image/png' : 'revisionfileimg.html',
+ 'image/gif' : 'revisionfileimg.html',
+ 'image/svg+xml' : 'revisionfileobj.html',
+ 'application/pdf' : 'revisionfileobj.html',
+ 'application/x-python' : 'revisionfiletxt.html',
+ 'application/x-perl' : 'revisionfiletxt.html',
+ }
+ template = mime_to_template.get(mimetype, None)
+ if not template:
+ if mimetype.startswith('text/'):
+ template = 'revisionfiletxt.html'
+ else:
+ template = 'revisionfilebin.html'
+ renderer.render(template,
+ filename=mtn.File(filename, revision),
+ page_title="File %s in revision %s" % (filename, revision.abbrev()),
+ revision=revision,
+ mimetype=mimetype,
+ contents=syntax.highlight(contents, language))
+
+class RevisionDownloadFile(RevisionPage):
+ def GET(self, revision, filename):
+ web.header('Content-Disposition', 'attachment; filename=%s' % filename)
+ revision = mtn.Revision(revision)
+ if not self.exists(revision):
+ return web.notfound()
+ fileid = RevisionPage.get_fileid(self, revision, filename)
+ if not fileid:
+ return web.notfound()
+ for idx, data in enumerate(ops.get_file(fileid)):
+ if idx == 0:
+ mimetype = mimehelp.lookup(filename, data)
+ web.header('Content-Type', mimetype)
+ sys.stdout.write(data)
+ sys.stdout.flush()
+
+class RevisionTar(RevisionPage):
+ def GET(self, revision):
+ # we'll output in the USTAR tar format; documentation taken from:
+ # http://en.wikipedia.org/wiki/Tar_%28file_format%29
+ revision = mtn.Revision(revision)
+ if not self.exists(revision):
+ return web.notfound()
+ web.header('Content-Disposition', 'attachment; filename=%s.tar' % revision)
+ web.header('Content-Type', 'application/x-tar')
+ manifest = [stanza for stanza in ops.get_manifest_of(revision)]
+ # for now; we might want to come up with something more interesting;
+ # maybe the branch name (but there might be multiple branches?)
+ basedirname = revision
+ tarobj = tarfile.open(mode="w", fileobj=sys.stdout)
+ dir_mode, file_mode = "0700", "0600"
+ certs = {}
+ for stanza in manifest:
+ stanza_type = stanza[0]
+ if stanza_type != 'file':
+ continue
+ filename, fileid = stanza[1], stanza[3]
+ filecontents = cStringIO.StringIO()
+ filesize = 0
+ for data in ops.get_file(fileid):
+ filesize += len(data)
+ filecontents.write(data)
+ ti = tarfile.TarInfo()
+ ti.name = os.path.join(revision, filename)
+ ti.mode, ti.type = 00600, tarfile.REGTYPE
+ ti.uid = ti.gid = 0
+ # determine the most recent of the content marks
+ content_marks = [t[1] for t in ops.get_content_changed(revision, filename)]
+ if len(content_marks) > 0:
+ # just pick one to make this faster
+ content_mark = content_marks[0]
+ since_epoch = timecert(ops.certs(content_mark)) - datetime.datetime.fromtimestamp(0)
+ ti.mtime = since_epoch.days * 24 * 60 * 60 + since_epoch.seconds
+ else:
+ ti.mtime = 0
+ ti.size = filesize
+ filecontents.seek(0)
+ tarobj.addfile(ti, filecontents)
+
+class RevisionBrowse(RevisionPage):
+ def GET(self, revision, path):
+ revision = mtn.Revision(revision)
+ if not self.exists(revision):
+ return web.notfound()
+ branches = RevisionPage.branches_for_rev(self, revision)
+ revisions = ops.get_revision(revision)
+
+ def components(path):
+ # NB: mtn internally uses '/' for paths, so we shouldn't use os.path.join()
+ # we should do things manually; otherwise we'll break on other platforms
+ # when we accidentally use \ or : or whatever.
+ #
+ # also, let's handle the case of spurious extra / characters
+ # whatever we return should make sense as '/'.join(rv)
+ rv = []
+ while path:
+ path = path.lstrip('/')
+ pc = path.split('/', 1)
+ if len(pc) == 2:
+ rv.append(pc[0])
+ path = pc[1]
+ else:
+ rv.append(pc[0])
+ path = ''
+ return rv
+
+ path = path or ""
+ path_components = components(path)
+ normalised_path = '/'.join(path_components)
+ # TODO: detect whether or not this exists and skip the following if it doesn't.
+ page_title = "Browsing revision %s: dir %s/" % (revision.abbrev(), normalised_path or '')
+
+ if len(branches) > 0:
+ if len(branches) == 1:
+ branch_plural = 'branch'
+ else:
+ branch_plural = 'branches'
+ page_title += " of %s %s" % (branch_plural, ', '.join(branches))
+
+ def cut_manifest_to_subdir():
+ manifest = map(None, ops.get_manifest_of(revision))
+ in_the_dir = False
+ for stanza in manifest:
+ stanza_type = stanza[0]
+ if stanza_type != "file" and stanza_type != "dir":
+ continue
+ this_path = stanza[1]
+
+ if not in_the_dir:
+ if stanza_type == "dir" and this_path == normalised_path:
+ in_the_dir = True
+ continue
+
+ this_path_components = components(this_path)
+ # debug(["inthedir", stanza_type, this_path, len(this_path_components), len(path_components)])
+ if stanza_type == "dir":
+ # are we still in our directory?
+ if len(this_path_components) > len(path_components) and \
+ this_path_components[:len(path_components)] == path_components:
+ # is this an immediate subdirectory of our directory?
+ if len(this_path_components) == len(path_components) + 1:
+ yield (stanza_type, this_path)
+ else:
+ in_the_dir = False
+ # and we've come out of the dir ne'er to re-enter, so..
+ break
+ elif stanza_type == "file" and len(this_path_components) == len(path_components) + 1:
+ yield (stanza_type, this_path)
+
+ def info_for_manifest(entry_iter):
+ # should probably limit memory usage (worst case is this gets huge)
+ # but for now, this is really a needed optimisation, as most of the
+ # time a single cert will be seen *many* times
+ certs = {}
+ certinfo = {}
+
+ def get_cert(revision):
+ if not certs.has_key(revision):
+ # subtle bug slipped in here; ops.cert() is a generator
+ # so we can't just store it in a cache!
+ certs[revision] = map(None, ops.certs(revision))
+ return certs[revision]
+
+ def _get_certinfo(revision):
+ author, ago, shortlog = None, None, None
+ for cert in get_cert(revision):
+ if cert[4] != 'name':
+ continue
+ name, value = cert[5], cert[7]
+ if name == "author":
+ author = mtn.Author(value)
+ elif name == "date":
+ revdate = common.parse_timecert(value)
+ ago = common.ago(revdate)
+ elif name == "changelog":
+ shortlog = quicklog(normalise_changelog(value), 40)
+ to_return = (author, ago, shortlog)
+ return [t or "" for t in to_return]
+
+ def get_certinfo(revision):
+ if not certinfo.has_key(revision):
+ certinfo[revision] = _get_certinfo(revision)
+ return certinfo[revision]
+
+ for stanza_type, this_path in entry_iter:
+ # determine the most recent of the content marks
+ content_marks = [t[1] for t in ops.get_content_changed(revision, this_path)]
+ for mark in content_marks:
+ get_cert(mark)
+ if len(content_marks):
+ content_marks.sort(lambda b, a: cmp(timecert(certs[a]), timecert(certs[b])))
+ content_mark = mtn.Revision(content_marks[0])
+ author, ago, shortlog = get_certinfo(content_mark)
+ else:
+ author, ago, shortlog, content_mark = mtn.Author(""), "", "", None
+ if stanza_type == "file":
+ file_obj = mtn.File(this_path, revision)
+ mime_type = mimehelp.lookup(this_path, "")
+ else:
+ file_obj = mtn.Dir(this_path, revision)
+ mime_type = 'inode/directory'
+ yield (stanza_type, file_obj, author, ago, content_mark, shortlog, mime_type)
+
+ def path_links(components):
+ # we always want a link to '/'
+ yield mtn.Dir('/', revision)
+ running_path = ""
+ for component in components:
+ running_path += component + "/"
+ yield mtn.Dir(running_path, revision)
+
+ def row_class():
+ while True:
+ yield "odd"
+ yield "even"
+
+ def mime_icon(mime_type):
+ return dynamic_join('/mimeicon/' + mime_type)
+
+ renderer.render('revisionbrowse.html',
+ branches=branches,
+ branch_links=', '.join([link(mtn.Branch(b)).html() for b in branches]),
+ path=path,
+ page_title=page_title,
+ revision=revision,
+ path_links=path_links(path_components),
+ row_class=row_class(),
+ mime_icon=mime_icon,
+ entries=info_for_manifest(cut_manifest_to_subdir()))
+
+def ancestry_dot(revision):
+ def dot_escape(s):
+ # kinda paranoid, should probably revise later
+ permitted=string.digits + string.letters + ' -<>-:,address@hidden&.+_~?/'
+ return ''.join([t for t in s if t in permitted])
+ revision = mtn.Revision(revision)
+ original_branches = []
+ for cert in ops.certs(revision):
+ if cert[4] == 'name' and cert[5] == 'branch':
+ original_branches.append(cert[7])
+
+ # strategy: we want to show information about this revision's place
+ # in the overall graph, both forward and back, for revision_count
+ # revisions in both directions (if possible)
+ #
+ # we will show propogates as dashed arcs
+ # otherwise, a full arc
+ #
+ # we'll show the arcs leading away from the revisions at either end,
+ # to make it clear that this is one part of a larger picture
+ #
+ # it'd be neat if someone wrote a google-maps style browser; I have
+ # some ideas as to how to approach this problem.
+
+ # revision graph is prone to change; someone could commit anywhere
+ # any time. so we'll have to generate this dotty file each time;
+ # let's write it into a temporary file (save some memory, no point
+ # keeping it about on disk) and sha1 hash the contents.
+ # we'll then see if ..png exists; if not, we'll
+ # generate it from the dot file
+
+ # let's be general, it's fairly symmetrical in either direction anyway
+ # I think we want to show a consistent view over a depth vertically; at the
+ # very least we should always show the dangling arcs
+ arcs = set()
+ nodes = set()
+ visited = set()
+
+ def visit_node(revision):
+ for node in ops.children(revision):
+ arcs.add((revision, node))
+ nodes.add(node)
+ for node in ops.parents(revision):
+ arcs.add((node, revision))
+ nodes.add(node)
+ visited.add(revision)
+
+ def graph_build_iter():
+ for node in (nodes - visited):
+ visit_node(node)
+
+ # stolen from monotone-viz
+ def colour_from_string(str):
+ def f(off):
+ return ord(hashval[off]) / 256.0
+ hashval = sha.new(str).digest()
+ hue = f(5)
+ li = f(1) * 0.15 + 0.55
+ sat = f(2) * 0.5 + .5
+ return ''.join(["%.2x" % int(x * 256) for x in hls_to_rgb(hue, li, sat)])
+
+ # for now, let's do three passes; seems to work fairly well
+ nodes.add(revision)
+ for i in xrange(3):
+ graph_build_iter()
+
+ graph = '''\
+digraph ancestry {
+ ratio=compress
+ nodesep=0.1
+ ranksep=0.2
+ edge [dir=forward];
+'''
+
+ # for each node, let's figure out it's colour, whether or not it's in our branch,
+ # and the label we'd give it; we need to look at all the nodes, as we need to know
+ # if off-screen nodes are propogates
+
+ node_colour = {}
+ node_label = {}
+ node_in_branch = {}
+
+ for node in nodes:
+ author, date = '', ''
+ branches = []
+ for cert in ops.certs(node):
+ if cert[4] == 'name' and cert[5] == 'date':
+ date = cert[7]
+ elif cert[4] == 'name' and cert[5] == 'author':
+ author = cert[7]
+ elif cert[4] == 'name' and cert[5] == 'branch':
+ branches.append(cert[7])
+ name, email = rfc822.parseaddr(author)
+ if name:
+ brief_name = name
+ else:
+ brief_name = author
+ node_label[node] = '%s on %s\\n%s' % (node.abbrev(),
+ dot_escape(date),
+ dot_escape(brief_name))
+ node_colour[node] = colour_from_string(author)
+ for branch in original_branches:
+ if branch in branches:
+ node_in_branch[node] = True
+ break
+
+ # draw visited nodes; other nodes are not actually shown
+ for node in visited:
+ line = ' "%s" ' % (node)
+ options = []
+ nodeopts = config.graphopts['nodeopts']
+ for option in nodeopts:
+ if option == 'fillcolor' and node_colour.has_key(node):
+ value = '#'+node_colour[node]
+ elif option == 'shape' and node == revision:
+ value = 'hexagon'
+ else:
+ value = nodeopts[option]
+ options.append('%s="%s"' % (option, value))
+ options.append('label="%s"' % (node_label[node]))
+ options.append('href="%s"' % link(node).uri())
+ line += '[' + ','.join(options) + ']'
+ graph += line + '\n'
+
+ for node in (nodes - visited):
+ graph += ' "%s" [style="invis",label=""]\n' % (node)
+
+ for (from_node, to_node) in arcs:
+ if node_in_branch.has_key(from_node) and node_in_branch.has_key(to_node):
+ style = "solid"
+ else:
+ style = "dashed"
+ graph += ' "%s"->"%s" [style="%s"]\n' % (from_node, to_node, style)
+ graph += '}'
+ return graph
+
+def ancestry_graph(revision):
+ dot_data = ancestry_dot(revision)
+ # okay, let's output the graph
+ graph_sha = sha.new(dot_data).hexdigest()
+ output_directory = os.path.join(config.graphopts['directory'], revision)
+ if not os.access(output_directory, os.R_OK):
+ os.mkdir(output_directory)
+ dot_file = os.path.join(output_directory, graph_sha+'.dot')
+ output_png = os.path.join(output_directory, 'graph.png')
+ output_imagemap = os.path.join(output_directory, 'imagemap.txt')
+ must_exist = (output_png, output_imagemap, dot_file)
+ if filter(lambda fname: not os.access(fname, os.R_OK), must_exist):
+ open(dot_file, 'w').write(dot_data)
+ command = "%s -Tcmapx -o %s -Tpng -o %s %s" % (config.graphopts['dot'],
+ output_imagemap,
+ output_png,
+ dot_file)
+ os.system(command)
+ return output_png, output_imagemap
+
+class RevisionGraph:
+ def GET(self, revision):
+ output_png, output_imagemap = ancestry_graph(revision)
+ if os.access(output_png, os.R_OK):
+ web.header('Content-Type', 'image/png')
+ sys.stdout.write(open(output_png).read())
+ else:
+ return web.notfound()
+
+class Json:
+ def GET(self, method, data):
+ print "Bah."
+
+class BranchHead:
+ def GET(self, head_method, proxy_to, branch, extra_path):
+ branch = mtn.Branch(branch)
+ valid = ('browse', 'file', 'downloadfile', 'info', 'tar', 'graph')
+ if not proxy_to in valid:
+ return web.notfound()
+ heads = [head for head in ops.heads(branch.name)]
+ if len(heads) == 0:
+ return web.notfound()
+ def proxyurl(revision):
+ return dynamic_join('/revision/' + proxy_to + '/' + revision + urllib.quote(extra_path))
+ if len(heads) == 1 or head_method == 'anyhead':
+ web.redirect(proxyurl(heads[0]))
+ else:
+ # present an option to the user to choose the head
+ anyhead = 'link ' % (dynamic_join('/branch/anyhead/' + proxy_to + '/' + branch.name))
+ head_links = []
+ for revision in heads:
+ author, date = '', ''
+ for cert in ops.certs(revision):
+ if cert[4] == 'name' and cert[5] == 'date':
+ date = cert[7]
+ elif cert[4] == 'name' and cert[5] == 'author':
+ author = mtn.Author(cert[7])
+ head_links.append('%s %s at %s' % (proxyurl(revision),
+ revision.abbrev(),
+ link(author).html(),
+ hq(date)))
+ renderer.render('branchchoosehead.html',
+ page_title="Branch %s" % branch.name,
+ branch=branch,
+ proxy_to=proxy_to,
+ anyhead=anyhead,
+ head_links=head_links)
+
+class MimeIcon:
+ def GET(self, type, sub_type):
+ mime_type = type+'/'+sub_type
+ icon_file = mimeicon.lookup(mime_type)
+ if icon_file:
+ web.header('Content-Type', 'image/png')
+ sys.stdout.write(open(icon_file).read())
+ else:
+ return web.notfound()
+
+class RobotsTxt:
+ def GET(self):
+ web.header('Content-Type', 'text/plain')
+ print "User-agent: *"
+ for revision_page in ['tar', 'downloadfile', 'graph']:
+ for access_method in ['/revision/', '/branch/head/', '/branch/anyhead/']:
+ print "Disallow:", access_method + revision_page
+
branch_re = r''
urls = (
- '/', 'Index',
- '/about', 'About'
+ r'/', 'Index', #done
+ r'/about', 'About', #done
+ r'/tags', 'Tags', #done
+ r'/help', 'Help', #done
+ r'/json/(A-Za-z)/(.*)', 'Json',
+
+ r'/revision/browse/('+mtn.revision_re+')/(.*)', 'RevisionBrowse',
+ r'/revision/browse/('+mtn.revision_re+')()', 'RevisionBrowse',
+ r'/revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')', 'RevisionDiff',
+ r'/revision/diff/('+mtn.revision_re+')/with/('+mtn.revision_re+')'+'/(.*)', 'RevisionDiff',
+ r'/revision/file/('+mtn.revision_re+')/(.*)', 'RevisionFile',
+ r'/revision/downloadfile/('+mtn.revision_re+')/(.*)', 'RevisionDownloadFile',
+ r'/revision/info/('+mtn.revision_re+')', 'RevisionInfo',
+ r'/revision/tar/('+mtn.revision_re+')', 'RevisionTar',
+ r'/revision/graph/('+mtn.revision_re+')', 'RevisionGraph',
+
+ r'/branch/changes/(.*)/from/(\d+)/to/(\d+)', 'HTMLBranchChanges',
+ r'/branch/changes/([^/]+)()()', 'HTMLBranchChanges',
+ r'/branch/changes/(.*)/from/(\d+)/to/(\d+)/rss', 'RSSBranchChanges',
+ r'/branch/changes/([^/]+)()()/rss', 'RSSBranchChanges',
+
+ # let's make it possible to access any function on the head revision
+ # through this proxy method; it'll return a redirect to the head revision
+ # with the specified function
+ r'/branch/(head)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
+ r'/branch/(anyhead)/([A-Za-z]+)/([^/]+)(.*)', 'BranchHead',
+
+ r'/static/(.*)', 'Static',
+ r'/robots.txt', 'RobotsTxt',
+ r'/mimeicon/([A-Za-z0-9][a-z0-9\-\+\.]*)/([A-Za-z0-9][a-z0-9\-\+\.]*)', 'MimeIcon',
)
if __name__ == '__main__':