Added multiple projects and non-default branches

This commit is contained in:
SoniEx2 2019-05-19 10:17:04 -03:00
parent 5a7baa6858
commit 4c47abad7e
1 changed files with 100 additions and 40 deletions

View File

@ -21,6 +21,7 @@ import click
import os import os
import subprocess import subprocess
import hashlib import hashlib
import hmac
import jinja2 import jinja2
# default HTML, can be overridden in $XDG_DATA_HOME/ganarchy/template.html or the $XDG_DATA_DIRS (TODO) # default HTML, can be overridden in $XDG_DATA_HOME/ganarchy/template.html or the $XDG_DATA_DIRS (TODO)
@ -47,14 +48,15 @@ TEMPLATE = """<!DOCTYPE html>
--> -->
<title>{{ project_title|e }}</title> <title>{{ project_title|e }}</title>
{% if project_desc %}<meta name="description" content="{{ project_desc|e }}" />{% endif %} {% if project_desc %}<meta name="description" content="{{ project_desc|e }}" />{% endif %}
<style type="text/css">.branchname { color: #808080; font-style: italic; }</style>
</head> </head>
<body> <body>
<h1>{{ project_title|e }}</h1> <h1>{{ project_title|e }}</h1>
<p>Tracking <span id="project_commit"><a href="web+ganarchy:{{ project_commit }}">{{ project_commit }}</a></span></p> <p>Tracking <span id="project_commit"><a href="web+ganarchy:{{ project_commit }}">{{ project_commit }}</a></span></p>
<div id="project_body"><p>{{ project_body|e|replace("\n\n", "</p><p>") }}</p></div> <div id="project_body"><p>{{ project_body|e|replace("\n\n", "</p><p>") }}</p></div>
<ul> <ul>
{% for url, msg, img in repos -%} {% for url, msg, img, branch in repos -%}
<li><a href="{{ url|e }}">{{ url|e }}</a>: {{ msg|e }}</li> <li><a href="{{ url|e }}">{{ url|e }}</a>{% if branch %} <span class="branchname">[{{ branch|e }}]</span>{% endif %}: {{ msg|e }}</li>
{%- endfor %} {%- endfor %}
</ul> </ul>
<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>Powered by <a href="https://ganarchy.autistic.space/">GAnarchy</a>. AGPLv3-licensed. <a href="https://cybre.tech/SoniEx2/ganarchy">Source Code</a>.</p>
@ -67,7 +69,25 @@ TEMPLATE = """<!DOCTYPE html>
""" """
MIGRATIONS = { MIGRATIONS = {
"test": ("-- apply", "-- revert", "does nothing") "better-project-management": (
(
"""ALTER TABLE "repos" ADD COLUMN "branch" TEXT""",
"""ALTER TABLE "repos" ADD COLUMN "project" TEXT""",
"""CREATE UNIQUE INDEX "repos_url_branch_project" ON "repos" ("url", "branch", "project")""",
"""CREATE INDEX "repos_project" ON "repos" ("project")""",
"""ALTER TABLE "repo_history" ADD COLUMN "branch" TEXT""",
"""ALTER TABLE "repo_history" ADD COLUMN "project" TEXT""",
"""CREATE INDEX "repo_history_url_branch_project" ON "repo_history" ("url", "branch", "project")"""),
(
"""DELETE FROM "repos" WHERE "branch" IS NOT NULL OR "project" IS NOT NULL""",
"""DELETE FROM "repo_history" WHERE "branch" IS NOT NULL OR "project" IS NOT NULL"""),
"supports multiple projects, and allows choosing non-default branches"
),
"test": (
("""-- apply""",),
("""-- revert""",),
"does nothing"
)
} }
try: try:
@ -96,12 +116,14 @@ def initdb():
os.makedirs(data_home, exist_ok=True) os.makedirs(data_home, exist_ok=True)
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''CREATE TABLE repos (url TEXT PRIMARY KEY, active INT)''') c.execute('''CREATE TABLE "repos" ("url" TEXT PRIMARY KEY, "active" INT, "branch" TEXT, "project" TEXT)''')
c.execute('''CREATE INDEX active_key ON repos (active)''') c.execute('''CREATE UNIQUE INDEX "repos_url_branch_project" ON "repos" ("url", "branch", "project")''')
c.execute('''CREATE TABLE repo_history (entry INTEGER PRIMARY KEY ASC AUTOINCREMENT, url TEXT, count INTEGER, head_commit TEXT)''') c.execute('''CREATE INDEX "repos_project" ON "repos" ("project")''')
c.execute('''CREATE INDEX url_key ON repo_history (url)''') c.execute('''CREATE INDEX "repos_active" ON "repos" ("active")''')
c.execute('''CREATE TABLE config (git_commit TEXT, base_url TEXT)''') c.execute('''CREATE TABLE "repo_history" ("entry" INTEGER PRIMARY KEY ASC AUTOINCREMENT, "url" TEXT, "count" INTEGER, "head_commit" TEXT, "branch" TEXT, "project" TEXT)''')
c.execute('''INSERT INTO config VALUES ('', '')''') c.execute('''CREATE INDEX "repo_history_url_branch_project" ON "repo_history" ("url", "branch", "project")''')
c.execute('''CREATE TABLE "config" ("git_commit" TEXT, "base_url" TEXT)''')
c.execute('''INSERT INTO "config" VALUES ('', '')''')
conn.commit() conn.commit()
conn.close() conn.close()
@ -114,7 +136,7 @@ def set_commit(commit):
raise click.BadArgumentUsage("COMMIT must be a git commit hash") raise click.BadArgumentUsage("COMMIT must be a git commit hash")
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''UPDATE config SET git_commit=?''', (commit,)) c.execute('''UPDATE "config" SET "git_commit"=?''', (commit,))
conn.commit() conn.commit()
conn.close() conn.close()
@ -124,53 +146,79 @@ def set_base_url(base_url):
"""Sets the GAnarchy instance's base URL.""" """Sets the GAnarchy instance's base URL."""
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''UPDATE config SET base_url=?''', (base_url,)) c.execute('''UPDATE "config" SET "base_url"=?''', (base_url,))
conn.commit() conn.commit()
conn.close() conn.close()
# TODO move --branch into here?
@ganarchy.group() @ganarchy.group()
def repo(): def repo():
"""Modifies repos to track.""" """Modifies repos to track."""
@repo.command() @repo.command()
@click.option('--branch', default=None, help="Sets the branch to be used for the repo")
@click.option('--project', default=None, help="Sets the project commit to be used for the repo")
@click.option('--disabled', default=False, is_flag=True, help="Mark the repo as disabled")
@click.argument('url') @click.argument('url')
def add(url): def add(branch, project, disabled, url):
"""Adds a repo to track.""" """Adds a repo to track."""
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''INSERT INTO repos VALUES (?, 0)''', (url,)) c.execute('''SELECT "git_commit", "base_url" FROM "config"''')
(project_commit, base_url) = c.fetchone()
if project_commit == project:
project = None
c.execute('''INSERT INTO "repos" ("url", "active", "branch", "project") VALUES (?, ?, ?, ?)''', (url, int(not disabled), branch, project))
conn.commit() conn.commit()
conn.close() conn.close()
@repo.command() @repo.command()
@click.option('--branch', default=None, help="Sets the branch to be used for the repo")
@click.option('--project', default=None, help="Sets the project commit to be used for the repo")
@click.argument('url') @click.argument('url')
def enable(url): def enable(branch, project, url):
"""Enables tracking of a repo.""" """Enables tracking of a repo."""
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''UPDATE repos SET active=1 WHERE url=?''', (url,)) c.execute('''SELECT "git_commit", "base_url" FROM "config"''')
(project_commit, base_url) = c.fetchone()
if project_commit == project:
project = None
c.execute('''UPDATE "repos" SET "active"=1 WHERE "url"=? AND "branch" IS ? AND "project" IS ?''', (url, branch, project))
conn.commit() conn.commit()
conn.close() conn.close()
@repo.command() @repo.command()
@click.option('--branch', default=None, help="Sets the branch to be used for the repo")
@click.option('--project', default=None, help="Sets the project commit to be used for the repo")
@click.argument('url') @click.argument('url')
def disable(url): def disable(branch, project, url):
"""Disables tracking of a repo.""" """Disables tracking of a repo."""
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''UPDATE repos SET active=0 WHERE url=?''', (url,)) c.execute('''SELECT "git_commit", "base_url" FROM "config"''')
(project_commit, base_url) = c.fetchone()
if project_commit == project:
project = None
c.execute('''UPDATE repos SET "active"=0 WHERE "url"=? AND "branch" IS ? AND "project" IS ?''', (url, branch, project))
conn.commit() conn.commit()
conn.close() conn.close()
@repo.command() @repo.command()
@click.option('--branch', default=None, help="Sets the branch to be used for the repo")
@click.option('--project', default=None, help="Sets the project commit to be used for the repo")
@click.argument('url') @click.argument('url')
def remove(url): def remove(branch, project, url):
"""Stops tracking a repo.""" """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.") 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") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''DELETE FROM repos WHERE url=?''', (url,)) c.execute('''SELECT "git_commit", "base_url" FROM "config"''')
c.execute('''DELETE FROM repo_history WHERE url=?''', (url,)) (project_commit, base_url) = c.fetchone()
if project_commit == project:
project = None
c.execute('''DELETE FROM "repos" WHERE "url"=? AND "branch" IS ? AND "project" IS ?''', (url, branch, project))
c.execute('''DELETE FROM "repo_history" WHERE "url"=? AND "branch" IS ? AND "project" IS ?''', (url, branch, project))
conn.commit() conn.commit()
conn.close() conn.close()
@ -188,7 +236,8 @@ def migrations():
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
click.echo(MIGRATIONS[migration][0]) click.echo(MIGRATIONS[migration][0])
c.execute(MIGRATIONS[migration][0]) for migration in MIGRATIONS[migration][0]:
c.execute(migration)
conn.commit() conn.commit()
conn.close() conn.close()
@ -199,7 +248,8 @@ def migrations():
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
click.echo(MIGRATIONS[migration][1]) click.echo(MIGRATIONS[migration][1])
c.execute(MIGRATIONS[migration][1]) for migration in MIGRATIONS[migration][1]:
c.execute(migration)
conn.commit() conn.commit()
conn.close() conn.close()
@ -216,16 +266,22 @@ def migrations():
migrations() migrations()
@ganarchy.command() @ganarchy.command()
def cron_target(): @click.argument('project', required=False)
def cron_target(project):
"""Runs ganarchy as a cron target.""" """Runs ganarchy as a cron target."""
def handle_target(url, project_commit): def handle_target(url, branch, project_commit):
branchname = "gan" + hashlib.sha256(url.encode("utf-8")).hexdigest() if not branch:
branchname = "gan" + hashlib.sha256(url.encode("utf-8")).hexdigest()
branch = "HEAD"
else:
branchname = "gan" + hmac.new(branch.encode("utf-8"), url.encode("utf-8"), "sha256").hexdigest()
branch = "refs/heads/" + branch
try: try:
pre_hash = subprocess.check_output(["git", "-C", cache_home, "show", branchname, "-s", "--format=%H", "--"], stderr=subprocess.DEVNULL).decode("utf-8").strip() pre_hash = subprocess.check_output(["git", "-C", cache_home, "show", branchname, "-s", "--format=%H", "--"], stderr=subprocess.DEVNULL).decode("utf-8").strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pre_hash = None pre_hash = None
try: try:
subprocess.check_output(["git", "-C", cache_home, "fetch", "-q", url, "+HEAD:" + branchname], stderr=subprocess.STDOUT) subprocess.check_output(["git", "-C", cache_home, "fetch", "-q", url, "+" + branch + ":" + branchname], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# This may error for various reasons, but some are important: dead links, etc # This may error for various reasons, but some are important: dead links, etc
click.echo(e.output, err=True) click.echo(e.output, err=True)
@ -246,34 +302,38 @@ def cron_target():
subprocess.call(["git", "-C", cache_home, "init", "-q"]) subprocess.call(["git", "-C", cache_home, "init", "-q"])
conn = sqlite3.connect(data_home + "/ganarchy.db") conn = sqlite3.connect(data_home + "/ganarchy.db")
c = conn.cursor() c = conn.cursor()
c.execute('''SELECT git_commit, base_url FROM config''') c.execute('''SELECT "git_commit", "base_url" FROM "config"''')
(project_commit, base_url) = c.fetchone() (project_commit, base_url) = c.fetchone()
if not base_url or not project_commit: if not base_url or not (project or project_commit):
click.echo("No base URL or project commit specified", err=True) click.echo("No base URL or project commit specified", err=True)
return return
if project_commit == project:
project = None
elif project is not None:
project_commit = project
entries = [] entries = []
generate_html = [] generate_html = []
for (e, url,) in c.execute("""SELECT max(e), url FROM (SELECT max(T1.entry) e, T1.url FROM repo_history T1 for (e, url, branch) in c.execute('''SELECT "max"("e"), "url", "branch" FROM (SELECT "max"("T1"."entry") "e", "T1"."url", "T1"."branch" FROM "repo_history" "T1"
WHERE (SELECT active FROM repos T2 WHERE url = T1.url) WHERE (SELECT "active" FROM "repos" "T2" WHERE "url" = "T1"."url" AND "branch" IS "T1"."branch" AND "project" IS ?1)
GROUP BY T1.url GROUP BY "T1"."url", "T1"."branch"
UNION UNION
SELECT null, T3.url FROM repos T3 WHERE active) SELECT null, "T3"."url", "T3"."branch" FROM "repos" "T3" WHERE "active" AND "project" IS ?1)
GROUP BY url ORDER BY e"""): GROUP BY "url" ORDER BY "e"''', (project,)):
result = handle_target(url, project_commit) result = handle_target(url, branch, project_commit)
if result is not None: if result is not None:
count, post_hash, msg = result count, post_hash, msg = result
entries.append((url, count, post_hash)) entries.append((url, count, post_hash, branch, project))
generate_html.append((url, msg, count)) generate_html.append((url, msg, count, branch))
# sort stuff twice because reasons # sort stuff twice because reasons
entries.sort(key=lambda x: x[1], reverse=True) entries.sort(key=lambda x: x[1], reverse=True)
generate_html.sort(key=lambda x: x[2], reverse=True) generate_html.sort(key=lambda x: x[2], reverse=True)
c.executemany('''INSERT INTO repo_history VALUES (NULL, ?, ?, ?)''', entries) c.executemany('''INSERT INTO "repo_history" ("url", "count", "head_commit", "branch", "project") VALUES (?, ?, ?, ?, ?)''', entries)
conn.commit() conn.commit()
html_entries = [] html_entries = []
for (url, msg, count) in generate_html: for (url, msg, count, branch) in generate_html:
history = c.execute('''SELECT count FROM repo_history WHERE url == ? ORDER BY entry ASC''', (url,)).fetchall() history = c.execute('''SELECT "count" FROM "repo_history" WHERE "url" = ? AND "branch" IS ? AND "project" IS ? ORDER BY "entry" ASC''', (url, branch, project)).fetchall()
# TODO process history into SVG # TODO process history into SVG
html_entries.append((url, msg, "")) html_entries.append((url, msg, "", branch))
template = jinja2.Template(TEMPLATE) template = jinja2.Template(TEMPLATE)
import re import re
project = subprocess.check_output(["git", "-C", cache_home, "show", project_commit, "-s", "--format=%B", "--"], stderr=subprocess.DEVNULL).decode("utf-8", "replace") project = subprocess.check_output(["git", "-C", cache_home, "show", project_commit, "-s", "--format=%B", "--"], stderr=subprocess.DEVNULL).decode("utf-8", "replace")