Added multiple projects and non-default branches
This commit is contained in:
parent
5a7baa6858
commit
4c47abad7e
140
ganarchy.py
140
ganarchy.py
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue