# This file is part of GAnarchy - decentralized project hub # Copyright (C) 2020 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 . """This module handles GAnarchy's database. Attributes: MIGRATIONS: Migrations. """ import sqlite3 import ganarchy.dirs import ganarchy.data # FIXME this should not be used directly because it's a pain. MIGRATIONS = { "toml-config": ( ( '''UPDATE "repo_history" SET "project" = (SELECT "git_commit" FROM "config") WHERE "project" IS NULL''', '''ALTER TABLE "repos" RENAME TO "repos_old"''', ), ( '''UPDATE "repo_history" SET "project" = NULL WHERE "project" = (SELECT "git_commit" FROM "config")''', '''ALTER TABLE "repos_old" RENAME TO "repos"''', ), "switches to toml config format. the old 'repos' " #cont "table is preserved as 'repos_old'" ), "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 " #cont "non-default branches" ), "test": ( ( '''-- apply''', ), ( '''-- revert''', ), "does nothing" ) } class Database: """A database connection/session, returned by ``connect_database``. Some methods may require repos to be loaded. """ def __init__(self, conn): self.conn = conn def initialize(self): """Initializes the database tables as expected by GAnarchy. """ c = self.conn.cursor() 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(''' CREATE INDEX "repo_history_url_branch_project" ON "repo_history" ("url", "branch", "project") ''') self.conn.commit() c.close() def apply_migration(self, migration): """Applies a migration, by name. WARNING: Destructive operation. Args: migration (str): The name of the migration. """ c = self.conn.cursor() for migration in MIGRATIONS[migration][0]: c.execute(migration) self.conn.commit() c.close() def revert_migration(self, migration): """Reverts a previously-applied migration, by name. WARNING: Destructive operation. Args: migration (str): The name of the migration. """ c = self.conn.cursor() for migration in MIGRATIONS[migration][1]: c.execute(migration) self.conn.commit() c.close() def load_repos(self, effective_repo_list): """Loads repos from repo list. Must be done once for each instance of Database. Args: effective_repo_list (ganarchy.data.DataSource): The data source for the repo list. """ c = self.conn.cursor() c.execute(''' CREATE TEMPORARY TABLE "repos" ( "url" TEXT PRIMARY KEY, "active" INT, "branch" TEXT, "project" TEXT ) ''') c.execute(''' CREATE UNIQUE INDEX "temp"."repos_url_branch_project" ON "repos" ("url", "branch", "project") ''') c.execute(''' CREATE INDEX "temp"."repos_project" ON "repos" ("project") ''') c.execute(''' CREATE INDEX "temp"."repos_active" ON "repos" ("active") ''') for repo in effective_repo_list.get_property_values( ganarchy.data.DataProperty.VCS_REPOS ): if repo.active: c.execute( '''INSERT INTO "repos" VALUES (?, ?, ?, ?)''', (repo.uri, 1, repo.branch, repo.project_commit) ) self.conn.commit() c.close() def insert_activity(self, project_commit, uri, branch, head, count): """Inserts activity of a repo-branch. Args: project_commit: The project commit. uri: The repo uri. branch: The branch. head: The latest known head commit. count: The number of new commits. """ self.insert_activities([(project_commit, uri, branch, head, count)]) def insert_activities(self, activities): """Inserts activities of repo-branches. Args: activities: List of tuple. The tuple must match up with the argument order specified by ``insert_activity``. """ c = self.conn.cursor() c.executemany( ''' INSERT INTO "repo_history" ( "project", "url", "branch", "head_commit", "count" ) VALUES (?, ?, ?, ?, ?) ''', activities ) conn.commit() c.close() def list_projects(self): """Lists loaded projects. Repos must be loaded first. Yields: str: Project commit of each project. """ c = self.conn.cursor() try: for (project,) in c.execute( '''SELECT DISTINCT "project" FROM "repos" ''' ): yield project finally: c.close() def list_repobranches(self, project_commit): """Lists repo-branches of a project. Repos must be loaded first. Results are sorted by recent activity. Args: project_commit: The project commit. Yields: A 3-tuple holding the URI, branch name, and last known head commit. """ c = self.conn.cursor() try: for (e, url, branch, head_commit) in c.execute( ''' SELECT "max"("e"), "url", "branch", "head_commit" FROM ( SELECT "max"("T1"."entry") "e", "T1"."url", "T1"."branch", "T1"."head_commit" FROM "repo_history" "T1" WHERE ( SELECT "active" FROM "repos" "T2" WHERE "url" = "T1"."url" AND "branch" IS "T1"."branch" AND "project" IS ?1 ) GROUP BY "T1"."url", "T1"."branch" UNION SELECT null, "T3"."url", "T3"."branch", null FROM "repos" "T3" WHERE "active" AND "project" IS ?1 ) GROUP BY "url" ORDER BY "e" ''', (project_commit,) ): yield url, branch, head_commit finally: c.close() def list_repobranch_activity(self, project_commit, uri, branch): """Lists activity of a repo-branch. Args: project_commit: The project commit. uri: The repo uri. branch: The branch. Returns: list of int: Number of commits between updates. """ c = self.conn.cursor() history = c.execute( ''' SELECT "count" FROM "repo_history" WHERE "url" = ? AND "branch" IS ? AND "project" IS ? ORDER BY "entry" ASC ''', (url, branch, project) ).fetchall() history = [x for [x] in history] c.close() return history def close(self): """Closes the database. """ self.conn.close() def connect_database(effective_config): """Opens the database specified by the given config. Args: effective_config (ganarchy.data.DataSource): The data source for the config. """ del effective_config # currently unused, intended for the future conn = sqlite3.connect(ganarchy.dirs.data_home + "/ganarchy.db") return Database(conn)