Rename to stahg-gopher.py - stahg-gopher - Static Mercurial page generator for gopher HTML hg clone https://bitbucket.org/iamleot/stahg-gopher DIR Log DIR Files DIR Refs DIR README DIR LICENSE --- DIR changeset 4e8d26dffa31a5ef7e5012f8060910e53452dee8 DIR parent 952e00b3a83e846a78c6f8669747d8fa6c957f82 HTML Author: Leonardo Taccari <iamleot@gmail.com> Date: Sun, 12 May 2019 22:21:34 Rename to stahg-gopher.py Diffstat: stahg-gopher.py | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ stahg.py | 337 -------------------------------------------------------- 2 files changed, 337 insertions(+), 337 deletions(-) --- diff -r 952e00b3a83e -r 4e8d26dffa31 stahg-gopher.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/stahg-gopher.py Sun May 12 22:21:34 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) diff -r 952e00b3a83e -r 4e8d26dffa31 stahg.py --- a/stahg.py Sun May 12 21:49:58 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,337 +0,0 @@ -#!/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)