2019-04-17 03:58:00 +02:00
#!/usr/bin/python3
# GAnarchy - project homepage generator
# Copyright (C) 2019 Soni L.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import sqlite3
import click
import os
import subprocess
import hashlib
import jinja2
# default HTML, can be overridden in $XDG_DATA_HOME/ganarchy/template.html or the $XDG_DATA_DIRS (TODO)
TEMPLATE = """ <!DOCTYPE html>
< html lang = " en " >
2019-04-20 22:12:39 +02:00
< head >
< meta charset = " utf-8 " / >
< ! - -
GAnarchy - project homepage generator
Copyright ( C ) 2019 Soni L .
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
along with this program . If not , see < https : / / www . gnu . org / licenses / > .
- - >
< title > { { project_title | e } } < / title >
{ % if project_desc % } < meta name = " description " content = " {{ project_desc|e }} " / > { % endif % }
< / head >
< body >
2019-04-21 00:27:50 +02:00
< h1 > { { project_title | e } } < / h1 >
2019-04-22 23:59:02 +02:00
< p > Tracking < span id = " project_commit " > < a href = " web+ganarchy:// {{ project_commit }} " > { { project_commit } } < / a > < / span > < / p >
2019-04-21 00:27:50 +02:00
< div id = " project_body " > < p > { { project_body | e | replace ( " \n \n " , " </p><p> " ) } } < / p > < / div >
2019-04-20 22:12:39 +02:00
< ul >
{ % for url , msg , img in repos - % }
< li > < a href = " {{ url|e }} " > { { url | e } } < / a > : { { msg | e } } < / li >
{ % - endfor % }
< / ul >
2019-04-22 23:59:02 +02:00
< p > Powered by < a href = " https://ganarchy.autistic.space/ " > GAnarchy < / a > . AGPLv3 - licensed . < a href = " https://cybre.tech/SoniEx2/ganarchy " > Source Code < / a > . < / p >
< p >
< a href = " / " > Main page < / a > .
< ! - - commented out because browsers suck : ( - - >
< ! - - < a href = " {{ base_url|e }} " onclick = " event.preventDefault(); navigator.registerProtocolHandler( ' web+ganarchy ' , this.href + ' project/ %s ' , ' GAnarchy ' ); " > Register web + ganarchy : / / handler < / a > . - - >
< / p >
2019-04-20 22:12:39 +02:00
< / body >
2019-04-17 03:58:00 +02:00
< / html >
"""
try :
data_home = os . environ [ ' XDG_DATA_HOME ' ]
except KeyError :
data_home = ' '
if not data_home :
data_home = os . environ [ ' HOME ' ] + ' /.local/share '
data_home = data_home + " /ganarchy "
try :
cache_home = os . environ [ ' XDG_CACHE_HOME ' ]
except KeyError :
cache_home = ' '
if not cache_home :
cache_home = os . environ [ ' HOME ' ] + ' /.cache '
cache_home = cache_home + " /ganarchy "
@click.group ( )
def ganarchy ( ) :
pass
@ganarchy.command ( )
def initdb ( ) :
""" Initializes the ganarchy database """
os . makedirs ( data_home , exist_ok = True )
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' CREATE TABLE repos (url TEXT PRIMARY KEY, active INT) ''' )
2019-04-19 14:52:28 +02:00
c . execute ( ''' CREATE INDEX active_key ON repos (active) ''' )
2019-04-17 03:58:00 +02:00
c . execute ( ''' CREATE TABLE repo_history (entry INTEGER PRIMARY KEY ASC AUTOINCREMENT, url TEXT, count INTEGER, head_commit TEXT) ''' )
2019-04-19 21:47:32 +02:00
c . execute ( ''' CREATE INDEX url_key ON repo_history (url) ''' )
2019-04-22 23:59:02 +02:00
c . execute ( ''' CREATE TABLE config (git_commit TEXT, base_url TEXT) ''' )
c . execute ( ''' INSERT INTO config VALUES ( ' ' , ' ' ) ''' )
2019-04-17 03:58:00 +02:00
conn . commit ( )
conn . close ( )
@ganarchy.command ( )
@click.argument ( ' commit ' )
def set_commit ( commit ) :
""" Sets the commit that represents the project """
import re
if not re . fullmatch ( " [a-fA-F0-9] {40} " , commit ) :
raise click . BadArgumentUsage ( " COMMIT must be a git commit hash " )
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' UPDATE config SET git_commit=? ''' , ( commit , ) )
conn . commit ( )
conn . close ( )
2019-04-22 23:59:02 +02:00
@ganarchy.command ( )
@click.argument ( ' base-url ' )
def set_base_url ( base_url ) :
""" Sets the GAnarchy instance ' s base URL """
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' UPDATE config SET base_url=? ''' , ( base_url , ) )
conn . commit ( )
conn . close ( )
2019-04-17 03:58:00 +02:00
@ganarchy.group ( )
def repo ( ) :
""" Modifies repos to track """
@repo.command ( )
@click.argument ( ' url ' )
def add ( url ) :
""" Adds a repo to track """
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' INSERT INTO repos VALUES (?, 0) ''' , ( url , ) )
conn . commit ( )
conn . close ( )
@repo.command ( )
@click.argument ( ' url ' )
def enable ( url ) :
""" Enables tracking of a repo """
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' UPDATE repos SET active=1 WHERE url=? ''' , ( url , ) )
conn . commit ( )
conn . close ( )
@repo.command ( )
@click.argument ( ' url ' )
def disable ( url ) :
""" Disables tracking of a repo """
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' UPDATE repos SET active=0 WHERE url=? ''' , ( url , ) )
conn . commit ( )
conn . close ( )
@repo.command ( )
@click.argument ( ' url ' )
def remove ( url ) :
""" Stops tracking a repo """
click . confirm ( " WARNING: This operation does not delete the commits associated with the given repo! Are you sure you want to continue? This operation cannot be undone. " )
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
c . execute ( ''' DELETE FROM repos WHERE url=? ''' , ( url , ) )
c . execute ( ''' DELETE FROM repo_history WHERE url=? ''' , ( url , ) )
conn . commit ( )
conn . close ( )
@ganarchy.command ( )
def cron_target ( ) :
""" Runs ganarchy as a cron target """
def handle_target ( url , project_commit ) :
2019-04-20 16:44:18 +02:00
branchname = " gan " + hashlib . sha256 ( url . encode ( " utf-8 " ) ) . hexdigest ( )
2019-04-17 03:58:00 +02:00
try :
2019-04-17 04:06:16 +02:00
pre_hash = subprocess . check_output ( [ " git " , " -C " , cache_home , " show " , branchname , " -s " , " --format= % H " , " -- " ] , stderr = subprocess . DEVNULL ) . decode ( " utf-8 " ) . strip ( )
2019-04-17 03:58:00 +02:00
except subprocess . CalledProcessError :
pre_hash = None
try :
subprocess . check_output ( [ " git " , " -C " , cache_home , " fetch " , " -q " , url , " +HEAD: " + branchname ] , stderr = subprocess . STDOUT )
except subprocess . CalledProcessError as e :
# This may error for various reasons, but some are important: dead links, etc
click . echo ( e . output , err = True )
return None
2019-04-17 04:06:16 +02:00
post_hash = subprocess . check_output ( [ " git " , " -C " , cache_home , " show " , branchname , " -s " , " --format= % H " , " -- " ] , stderr = subprocess . DEVNULL ) . decode ( " utf-8 " ) . strip ( )
2019-04-17 03:58:00 +02:00
if not pre_hash :
pre_hash = post_hash
try :
2019-04-17 04:06:16 +02:00
count = int ( subprocess . check_output ( [ " git " , " -C " , cache_home , " rev-list " , " --count " , pre_hash + " .. " + post_hash , " -- " ] ) . decode ( " utf-8 " ) . strip ( ) )
2019-04-17 03:58:00 +02:00
except subprocess . CalledProcessError :
count = 0 # force-pushed
try :
subprocess . check_call ( [ " git " , " -C " , cache_home , " merge-base " , " --is-ancestor " , project_commit , branchname ] , stdout = subprocess . DEVNULL , stderr = subprocess . DEVNULL )
2019-04-17 04:06:16 +02:00
return count , post_hash , subprocess . check_output ( [ " git " , " -C " , cache_home , " show " , branchname , " -s " , " --format= % B " , " -- " ] , stderr = subprocess . DEVNULL ) . decode ( " utf-8 " , " replace " )
2019-04-17 03:58:00 +02:00
except subprocess . CalledProcessError :
return None
os . makedirs ( cache_home , exist_ok = True )
subprocess . call ( [ " git " , " -C " , cache_home , " init " , " -q " ] )
conn = sqlite3 . connect ( data_home + " /ganarchy.db " )
c = conn . cursor ( )
2019-04-22 23:59:02 +02:00
c . execute ( ''' SELECT git_commit, base_url FROM config ''' )
( project_commit , base_url ) = c . fetchone ( )
if not base_url or not project_commit :
click . echo ( " No base URL or project commit specified " , err = True )
return
2019-04-17 03:58:00 +02:00
entries = [ ]
generate_html = [ ]
2019-04-20 03:32:15 +02:00
for ( e , url , ) in c . execute ( """ SELECT max(e), url FROM (SELECT max(T1.entry) e, T1.url FROM repo_history T1
WHERE ( SELECT active FROM repos T2 WHERE url = T1 . url )
GROUP BY T1 . url
UNION
SELECT null , T3 . url FROM repos T3 WHERE active )
GROUP BY url ORDER BY e """ ):
2019-04-17 03:58:00 +02:00
result = handle_target ( url , project_commit )
if result is not None :
count , post_hash , msg = result
entries . append ( ( url , count , post_hash ) )
2019-04-18 03:29:56 +02:00
generate_html . append ( ( url , msg , count ) )
2019-04-19 21:42:59 +02:00
# sort stuff twice because reasons
entries . sort ( key = lambda x : x [ 1 ] , reverse = True )
generate_html . sort ( key = lambda x : x [ 2 ] , reverse = True )
2019-04-17 03:58:00 +02:00
c . executemany ( ''' INSERT INTO repo_history VALUES (NULL, ?, ?, ?) ''' , entries )
conn . commit ( )
html_entries = [ ]
2019-04-18 03:29:56 +02:00
for ( url , msg , count ) in generate_html :
2019-04-17 03:58:00 +02:00
history = c . execute ( ''' SELECT count FROM repo_history WHERE url == ? ORDER BY entry ASC ''' , ( url , ) ) . fetchall ( )
# TODO process history into SVG
html_entries . append ( ( url , msg , " " ) )
template = jinja2 . Template ( TEMPLATE )
2019-04-20 22:12:39 +02:00
import re
project = subprocess . check_output ( [ " git " , " -C " , cache_home , " show " , project_commit , " -s " , " --format= % B " , " -- " ] , stderr = subprocess . DEVNULL ) . decode ( " utf-8 " , " replace " )
project_title , project_desc = ( lambda x : x . groups ( ) if x is not None else ( ' ' , None ) ) ( re . fullmatch ( ' ^ \\ [Project \\ ] \ s+(.+?)(?: \n \n (.+))?$ ' , project , flags = re . ASCII | re . DOTALL | re . IGNORECASE ) )
if not project_title . strip ( ) :
project_title , project_desc = ( " Error parsing project commit " , ) * 2
if project_desc :
project_desc = project_desc . strip ( )
2019-04-21 00:27:50 +02:00
click . echo ( template . render ( project_title = project_title ,
project_desc = project_desc ,
project_body = project ,
project_commit = project_commit ,
2019-04-22 23:59:02 +02:00
repos = html_entries ,
base_url = base_url ) )
2019-04-17 03:58:00 +02:00
if __name__ == " __main__ " :
ganarchy ( )