summary refs log tree commit diff stats
path: root/ganarchy/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'ganarchy/core.py')
-rw-r--r--ganarchy/core.py165
1 files changed, 165 insertions, 0 deletions
diff --git a/ganarchy/core.py b/ganarchy/core.py
new file mode 100644
index 0000000..cf5edd4
--- /dev/null
+++ b/ganarchy/core.py
@@ -0,0 +1,165 @@
+# 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 <https://www.gnu.org/licenses/>.
+
+import ganarchy.git
+import ganarchy.dirs
+
+# Currently we only use one git repo, at CACHE_HOME
+# TODO optimize
+GIT = ganarchy.git.Git(ganarchy.dirs.CACHE_HOME)
+
+class Repo:
+    def __init__(self, dbconn, project_commit, url, branch, head_commit, list_metadata=False):
+        self.url = url
+        self.branch = branch
+        self.project_commit = project_commit
+        self.erroring = False
+
+        if not branch:
+            self.branchname = "gan" + hashlib.sha256(url.encode("utf-8")).hexdigest()
+            self.head = "HEAD"
+        else:
+            self.branchname = "gan" + hmac.new(branch.encode("utf-8"), url.encode("utf-8"), "sha256").hexdigest()
+            self.head = "refs/heads/" + branch
+
+        if head_commit:
+            self.hash = head_commit
+        else:
+            try: # FIXME should we even do this?
+                self.hash = GIT.get_hash(self.branchname)
+            except ganarchy.git.GitError:
+                self.erroring = True
+                self.hash = None
+
+        self.message = None
+        if list_metadata:
+            try:
+                self.update_metadata()
+            except ganarchy.git.GitError:
+                self.erroring = True
+                pass
+
+    def update_metadata(self):
+        self.message = GIT.get_commit_message(self.branchname)
+
+    def update(self, updating=True):
+        """Updates the git repo, returning new metadata.
+        """
+        if updating:
+            try:
+                GIT.force_fetch(self.url, self.head, self.branchname)
+            except ganarchy.git.GitError as e:
+                # This may error for various reasons, but some
+                # are important: dead links, etc
+                click.echo(e.output, err=True)
+                self.erroring = True
+                return None
+        pre_hash = self.hash
+        try:
+            post_hash = GIT.get_hash(self.branchname)
+        except ganarchy.git.GitError as e:
+            # This should never happen, but maybe there's some edge cases?
+            # TODO check
+            self.erroring = True
+            return None
+        self.hash = post_hash
+        if not pre_hash:
+            pre_hash = post_hash
+        count = GIT.get_count(pre_hash, post_hash)
+        try:
+            if updating:
+                GIT.check_history(self.branchname, self.project_commit)
+            self.update_metadata()
+            return count
+        except ganarchy.git.GitError as e:
+            click.echo(e, err=True)
+            self.erroring = True
+            return None
+
+class Project:
+    def __init__(self, dbconn, project_commit, list_repos=False):
+        self.commit = project_commit
+        self.refresh_metadata()
+        self.repos = None
+        if list_repos:
+            self.list_repos(dbconn)
+
+    def list_repos(self, dbconn):
+        repos = []
+        with dbconn:
+            for (e, url, branch, head_commit) in dbconn.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"''', (self.commit,)):
+                repos.append(Repo(dbconn, self.commit, url, branch, head_commit))
+        self.repos = repos
+
+    def refresh_metadata(self):
+        try:
+            project = GIT.get_commit_message(self.commit)
+            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(): # FIXME
+                project_title, project_desc = ("Error parsing project commit",)*2
+            # if project_desc: # FIXME
+            #     project_desc = project_desc.strip()
+            self.commit_body = project
+            self.title = project_title
+            self.description = project_desc
+        except ganarchy.git.GitError:
+            self.commit_body = None
+            self.title = None
+            self.description = None
+
+    def update(self, updating=True):
+        # TODO? check if working correctly
+        results = [(repo, repo.update(updating)) for repo in self.repos]
+        self.refresh_metadata()
+        return results
+
+class GAnarchy:
+    def __init__(self, dbconn, config, list_projects=False, list_repos=False):
+        base_url = config.base_url
+        title = config.title
+        if not base_url:
+            # FIXME use a more appropriate error type
+            raise ValueError
+        if not title:
+            title = "GAnarchy on " + urlparse(base_url).hostname
+        self.title = title
+        self.base_url = base_url
+        # load config onto DB
+        c = dbconn.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 (project_commit, repos) in config.projects.items():
+            for (repo_url, branches) in repos.items():
+                for (branchname, options) in branches.items():
+                    if options['active']: # no need to insert inactive repos since they get ignored anyway
+                        c.execute('''INSERT INTO "repos" VALUES (?, ?, ?, ?)''', (repo_url, 1, branchname, project_commit))
+        dbconn.commit()
+        if list_projects:
+            projects = []
+            with dbconn:
+                for (project,) in dbconn.execute('''SELECT DISTINCT "project" FROM "repos" '''):
+                    projects.append(Project(dbconn, project, list_repos=list_repos))
+            projects.sort(key=lambda project: project.title) # sort projects by title
+            self.projects = projects
+        else:
+            self.projects = None