Initial commit - stahg-gopher - Static Mercurial page generator for gopher
(HTM) hg clone https://bitbucket.org/iamleot/stahg-gopher
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
(DIR) changeset 952e00b3a83e846a78c6f8669747d8fa6c957f82
(HTM) Author: Leonardo Taccari <iamleot@gmail.com>
Date: Sun, 12 May 2019 21:49:58
Initial commit
Diffstat:
README | 7 +
stahg.py | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 344 insertions(+), 0 deletions(-)
---
diff -r 000000000000 -r 952e00b3a83e README
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README Sun May 12 21:49:58 2019 +0200
@@ -0,0 +1,7 @@
+stahg-gopher
+============
+
+Static Mercurial page generator for gopher.
+
+stahg-gopher is a stagit-gopher clone for Mercurial. It generates
+pages in the geomyidae .gph file format.
diff -r 000000000000 -r 952e00b3a83e stahg.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/stahg.py Sun May 12 21:49:58 2019 +0200
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3.7
+
+#
+# Copyright (c) 2019 Leonardo Taccari
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+
+import datetime
+import os
+import shutil
+import stat
+
+import hglib
+
+
+LICENSE_FILES = [ 'LICENSE', 'LICENSE.md', 'COPYING' ]
+README_FILES = [ 'README', 'README.md' ]
+
+
+def gph_escape_entry(text):
+ """Render text entry `[...]' by escaping/translating characters"""
+ escaped_text = text.expandtabs().replace('|', '\|')
+
+ return escaped_text
+
+
+def gph_escape_text(text):
+ """Render text to .gph by escaping/translating characters"""
+ escaped_text = []
+
+ for line in text.expandtabs().splitlines():
+ # add leading 't' if needed
+ if len(line) > 0 and line[0] == 't':
+ line = 't' + line
+
+ escaped_text.append(line)
+
+ return '\n'.join(escaped_text)
+
+
+def shorten(text, n=80):
+ """Shorten text to the first `n' character of first line"""
+ s, _, _ = text.partition('\n')
+
+ if len(s) > n:
+ s = s[:n - 1] + '…'
+
+ return s
+
+
+def rshorten(text, n=80):
+ """Shorten text to the last `n' character of first line"""
+ s, _, _ = text.partition('\n')
+
+ if len(s) > n:
+ s = '…' + s[- (n - 1):]
+
+ return s
+
+
+def author_name(author):
+ """Given an author `Name <email>' extract their name"""
+ name, _, _ = author.rpartition(' <')
+
+ return name
+
+
+def author_email(author):
+ """Given an author `Name <email>' extract their email"""
+ _, _, email = author.rpartition(' <')
+ email = email.rstrip('>')
+
+ return email
+
+
+class Stahg:
+ def __init__(self, base_prefix='', limit=None):
+ self.base_prefix = base_prefix
+ self.client = None
+ self.description = ''
+ self.license = None
+ self.limit = limit
+ self.readme = None
+ self.repodir = ''
+ self.repository = ''
+ self.url = ''
+
+
+ def open(self, repodir):
+ """Open repository in repodir"""
+ self.repodir = os.path.normpath(repodir)
+ self.client = hglib.open(self.repodir)
+ self.base_prefix = base_prefix
+ self.repository = os.path.basename(self.repodir)
+
+ try:
+ for _, k, value in self.client.config([b'web']):
+ if k == 'description':
+ self.description = value
+ break
+ except:
+ self.description = \
+ "Unnamed repository, adjust .hg/hgrc `[web]' section, `description' key"
+
+ # XXX: For repository with a lot of files this is suboptimal...
+ # XXX: Is there a simpler way to check for that?
+ for e in self.client.manifest(rev=b'tip'):
+ fpath = e[4].decode()
+
+ # file paths are sorted, break as soon as possible
+ if fpath > max(LICENSE_FILES) and fpath > max(README_FILES):
+ break
+
+ if fpath in LICENSE_FILES:
+ self.license = fpath
+ if fpath in README_FILES:
+ self.readme = fpath
+
+
+ def close(self):
+ """Close repository"""
+ self.client.close()
+
+
+ def menu(self):
+ """Generate menu for .gph files"""
+ bp = gph_escape_entry(self.base_prefix)
+
+ m = '[1|Log|' + bp + '/log.gph|server|port]\n' + \
+ '[1|Files|' + bp + '/files.gph|server|port]'
+
+ if self.readme:
+ m += '\n[1|README|' + bp + '/file/{file}.gph|server|port]'.format(
+ file=self.readme)
+
+ if self.license:
+ m += '\n[1|LICENSE|' + bp + '/file/{file}.gph|server|port]'.format(
+ file=self.license)
+
+ return m
+
+
+ def title(self, text):
+ """Generate title for .gph files"""
+ return gph_escape_text(
+ ' - '.join([text, self.repository, self.description]))
+
+
+ def log(self):
+ """Generate log.gph with latest commits"""
+ bp = gph_escape_entry(self.base_prefix)
+ fname = 'log.gph'
+
+ with open(fname, 'w') as f:
+ print(self.title('Log'), file=f)
+ print(self.menu(), file=f)
+ print('---', file=f)
+
+ print('{:16} {:40} {}'.format('Date', 'Commit message', 'Author'),
+ file=f)
+ for i, e in enumerate(self.client.log()):
+ if self.limit and i > self.limit:
+ print(' More commits remaining [...]',
+ file=f)
+ break
+ print('[1|{desc}|{path}|server|port]'.format(
+ desc='{date:16} {commit_message:40} {author}'.format(
+ date=e.date.strftime('%Y-%m-%d %H:%M'),
+ commit_message=gph_escape_entry(shorten(e.desc.decode(), 40)),
+ author=author_name(e.author.decode())),
+ path='{base_path}/commit/{changeset}.gph'.format(
+ base_path=bp,
+ changeset=e.node.decode())), file=f)
+
+
+ def files(self):
+ """Generate files.gph with links to all files in `tip'"""
+ bp = gph_escape_entry(self.base_prefix)
+ fname = 'files.gph'
+
+ with open(fname, 'w') as f:
+ print(self.title('Files'), file=f)
+ print(self.menu(), file=f)
+ print('---', file=f)
+
+ print('{:10} {:68}'.format('Mode', 'Name'), file=f)
+
+ for e in self.client.manifest(rev=b'tip'):
+ print('[1|{desc}|{path}|server|port]'.format(
+ desc='{mode:10} {name:68}'.format(
+ mode=stat.filemode(int(e[1].decode(), base=8)),
+ name=gph_escape_entry(e[4].decode())),
+ path='{base_path}/file/{file}.gph'.format(
+ base_path=bp,
+ file=gph_escape_entry(e[4].decode()))), file=f)
+
+
+ def refs(self):
+ """Generate refs.gph listing all branches and tags"""
+ pass # TODO
+
+
+ def commit(self, changeset):
+ """Generate commit/<changeset>.gph with commit message and diff"""
+ bp = gph_escape_entry(self.base_prefix)
+ c = self.client[changeset]
+ fname = 'commit/{changeset}.gph'.format(changeset=c.node().decode())
+
+ with open(fname, 'w') as f:
+ print(self.title(shorten(c.description().decode(), 80)), file=f)
+ print(self.menu(), file=f)
+ print('---', file=f)
+
+ print('[1|{desc}|{path}|server|port]'.format(
+ desc='changeset {changeset}'.format(changeset=c.node().decode()),
+ path='{base_path}/commit/{changeset}.gph'.format(
+ base_path=bp,
+ changeset=c.node().decode())), file=f)
+
+ for p in c.parents():
+ if p.node() == b'0000000000000000000000000000000000000000':
+ continue
+ print('[1|{desc}|{path}|server|port]'.format(
+ desc='parent {changeset}'.format(changeset=p.node().decode()),
+ path='{base_path}/commit/{changeset}.gph'.format(
+ base_path=bp,
+ changeset=p.node().decode())), file=f)
+
+ print('[h|Author: {author}|URL:mailto:{email}|server|port]'.format(
+ author=gph_escape_entry(c.author().decode()),
+ email=gph_escape_entry(author_email(c.author().decode()))), file=f)
+
+ print('Date: {date}'.format(
+ date=c.date().strftime('%a, %e %b %Y %H:%M:%S %z')), file=f)
+
+ print(file=f)
+ print(gph_escape_text(c.description().decode()), file=f)
+ print(file=f)
+
+ print('Diffstat:', file=f)
+ print(gph_escape_text(self.client.diff(change=c.node(), stat=True).decode().rstrip()),
+ file=f)
+ print('---', file=f)
+
+ print(gph_escape_text(self.client.diff(change=c.node()).decode()),
+ file=f)
+
+
+ def file(self, file):
+ """Generate file/<file>.gph listing <file> at `tip'"""
+ bp = gph_escape_entry(self.base_prefix)
+ fname = 'file/{file}.gph'.format(file=file.decode())
+ os.makedirs(os.path.dirname(fname), exist_ok=True)
+
+ with open(fname, 'w') as f:
+ print(self.title(os.path.basename(file.decode())), file=f)
+ print(self.menu(), file=f)
+ print('---', file=f)
+
+ print('{filename}'.format(
+ filename=os.path.basename(file.decode())), file=f)
+ print('---', file=f)
+
+ files = [self.client.root() + os.sep.encode() + file]
+ for num, line in enumerate(self.client.cat(files).decode().splitlines(), start=1):
+ print('{num:6d} {line}'.format(
+ num=num,
+ line=gph_escape_text(line)), file=f)
+
+
+if __name__ == '__main__':
+ import getopt
+ import sys
+
+ def usage():
+ print('usage: {} [-b baseprefix] [-l commits] repodir'.format(
+ sys.argv[0]))
+ exit(1)
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'b:l:')
+ except:
+ usage()
+
+ if len(args) != 1:
+ usage()
+
+ base_prefix = ''
+ limit = None
+ for o, a in opts:
+ if o == '-b':
+ base_prefix = a
+ elif o == '-l':
+ limit = int(a)
+
+ repodir = args[0]
+
+ sh = Stahg(base_prefix=base_prefix, limit=limit)
+ sh.open(repodir)
+
+ sh.log()
+ sh.files()
+ sh.refs()
+
+ shutil.rmtree('file', ignore_errors=True)
+ os.makedirs('file', exist_ok=True)
+ for e in sh.client.manifest(rev=b'tip'):
+ sh.file(e[4])
+
+ os.makedirs('commit', exist_ok=True)
+ for e in sh.client.log():
+ if os.path.exists('commit/{changeset}.gph'.format(changeset=e.node.decode())):
+ break
+ sh.commit(e.node)