|
| 1 | +import os |
| 2 | +import logging |
| 3 | +from git import Commit |
| 4 | +from falkordb import FalkorDB, Node |
| 5 | +from typing import List, Optional |
| 6 | + |
| 7 | +# Configure logging |
| 8 | +logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s') |
| 9 | + |
| 10 | +class GitGraph(): |
| 11 | + """ |
| 12 | + Represents a git commit graph |
| 13 | + nodes are commits where one commit leads to its parents and children |
| 14 | + edges contains queries and parameters for transitioning the code-graph |
| 15 | + from the current commit to parent / child |
| 16 | + """ |
| 17 | + |
| 18 | + def __init__(self, name: str): |
| 19 | + |
| 20 | + self.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'), |
| 21 | + port=os.getenv('FALKORDB_PORT', 6379), |
| 22 | + username=os.getenv('FALKORDB_USERNAME', None), |
| 23 | + password=os.getenv('FALKORDB_PASSWORD', None)) |
| 24 | + |
| 25 | + self.g = self.db.select_graph(name) |
| 26 | + |
| 27 | + # create indicies |
| 28 | + # index commit hash |
| 29 | + try: |
| 30 | + self.g.create_node_range_index("Commit", "hash") |
| 31 | + except Exception: |
| 32 | + pass |
| 33 | + |
| 34 | + def _commit_from_node(self, node:Node) -> dict: |
| 35 | + """ |
| 36 | + Returns a dict representing a commit node |
| 37 | + """ |
| 38 | + |
| 39 | + return {'hash': node.properties['hash'], |
| 40 | + 'date': node.properties['date'], |
| 41 | + 'author': node.properties['author'], |
| 42 | + 'message': node.properties['message']} |
| 43 | + |
| 44 | + def add_commit(self, commit: Commit) -> None: |
| 45 | + """ |
| 46 | + Add a new commit to the graph |
| 47 | + """ |
| 48 | + date = commit.committed_date |
| 49 | + author = commit.author.name |
| 50 | + hexsha = commit.hexsha |
| 51 | + message = commit.message |
| 52 | + logging.info(f"Adding commit {hexsha}: {message}") |
| 53 | + |
| 54 | + q = "MERGE (c:Commit {hash: $hash, author: $author, message: $message, date: $date})" |
| 55 | + params = {'hash': hexsha, 'author': author, 'message': message, 'date': date} |
| 56 | + self.g.query(q, params) |
| 57 | + |
| 58 | + def list_commits(self) -> List[Node]: |
| 59 | + """ |
| 60 | + List all commits |
| 61 | + """ |
| 62 | + |
| 63 | + q = "MATCH (c:Commit) RETURN c ORDER BY c.date" |
| 64 | + result_set = self.g.query(q).result_set |
| 65 | + |
| 66 | + return [self._commit_from_node(row[0]) for row in result_set] |
| 67 | + |
| 68 | + def get_commits(self, hashes: List[str]) -> List[dict]: |
| 69 | + logging.info(f"Searching for commits {hashes}") |
| 70 | + |
| 71 | + q = """MATCH (c:Commit) |
| 72 | + WHERE c.hash IN $hashes |
| 73 | + RETURN c""" |
| 74 | + |
| 75 | + params = {'hashes': hashes} |
| 76 | + res = self.g.query(q, params).result_set |
| 77 | + |
| 78 | + commits = [] |
| 79 | + for row in res: |
| 80 | + commit = self._commit_from_node(row[0]) |
| 81 | + commits.append(commit) |
| 82 | + |
| 83 | + logging.info(f"retrived commits: {commits}") |
| 84 | + return commits |
| 85 | + |
| 86 | + def get_child_commit(self, parent) -> Optional[dict]: |
| 87 | + q = """MATCH (c:Commit {hash: $parent})-[:CHILD]->(child: Commit) |
| 88 | + RETURN child""" |
| 89 | + |
| 90 | + res = self.g.query(q, {'parent': parent}).result_set |
| 91 | + |
| 92 | + if len(res) > 0: |
| 93 | + assert(len(res) == 1) |
| 94 | + return self._commit_from_node(res[0][0]) |
| 95 | + |
| 96 | + return None |
| 97 | + |
| 98 | + def connect_commits(self, child: str, parent: str) -> None: |
| 99 | + """ |
| 100 | + connect commits via both PARENT and CHILD edges |
| 101 | + """ |
| 102 | + |
| 103 | + logging.info(f"Connecting commits {child} -PARENT-> {parent}") |
| 104 | + logging.info(f"Connecting commits {parent} -CHILD-> {child}") |
| 105 | + |
| 106 | + q = """MATCH (child :Commit {hash: $child_hash}), (parent :Commit {hash: $parent_hash}) |
| 107 | + MERGE (child)-[:PARENT]->(parent) |
| 108 | + MERGE (parent)-[:CHILD]->(child)""" |
| 109 | + |
| 110 | + params = {'child_hash': child, 'parent_hash': parent} |
| 111 | + |
| 112 | + self.g.query(q, params) |
| 113 | + |
| 114 | + |
| 115 | + def set_parent_transition(self, child: str, parent: str, queries: [str], params: [str]) -> None: |
| 116 | + """ |
| 117 | + Sets the queries and parameters needed to transition the code-graph |
| 118 | + from the child commit to the parent commit |
| 119 | + """ |
| 120 | + |
| 121 | + q = """MATCH (child :Commit {hash: $child})-[e:PARENT]->(parent :Commit {hash: $parent}) |
| 122 | + SET e.queries = $queries, e.params = $params""" |
| 123 | + |
| 124 | + _params = {'child': child, 'parent': parent, 'queries': queries, 'params': params} |
| 125 | + |
| 126 | + self.g.query(q, _params) |
| 127 | + |
| 128 | + |
| 129 | + def set_child_transition(self, child: str, parent: str, queries: [str], params: [str]) -> None: |
| 130 | + """ |
| 131 | + Sets the queries and parameters needed to transition the code-graph |
| 132 | + from the parent commit to the child commit |
| 133 | + """ |
| 134 | + |
| 135 | + q = """MATCH (parent :Commit {hash: $parent})-[e:CHILD]->(child :Commit {hash: $child}) |
| 136 | + SET e.queries = $queries, e.params = $params""" |
| 137 | + |
| 138 | + _params = {'child': child, 'parent': parent, 'queries': queries, 'params': params} |
| 139 | + |
| 140 | + self.g.query(q, _params) |
| 141 | + |
| 142 | + |
| 143 | + def get_parent_transitions(self, child: str, parent: str) -> List[tuple[str: dict]]: |
| 144 | + """ |
| 145 | + Get queries and parameters transitioning from child commit to parent commit |
| 146 | + """ |
| 147 | + q = """MATCH path = (:Commit {hash: $child_hash})-[:PARENT*]->(:Commit {hash: $parent_hash}) |
| 148 | + WITH path |
| 149 | + LIMIT 1 |
| 150 | + UNWIND relationships(path) AS e |
| 151 | + WITH e |
| 152 | + WHERE e.queries is not NULL |
| 153 | + RETURN collect(e.queries), collect(e.params) |
| 154 | + """ |
| 155 | + |
| 156 | + res = self.g.query(q, {'child_hash': child, 'parent_hash': parent}).result_set |
| 157 | + |
| 158 | + return (res[0][0], res[0][1]) |
| 159 | + |
| 160 | + |
| 161 | + def get_child_transitions(self, child: str, parent: str) -> List[tuple[str: dict]]: |
| 162 | + """ |
| 163 | + Get queries and parameters transitioning from parent commit to child commit |
| 164 | + """ |
| 165 | + q = """MATCH path = (:Commit {hash: $parent_hash})-[:CHILD*]->(:Commit {hash: $child_hash}) |
| 166 | + WITH path |
| 167 | + LIMIT 1 |
| 168 | + UNWIND relationships(path) AS e |
| 169 | + WITH e |
| 170 | + WHERE e.queries is not NULL |
| 171 | + RETURN collect(e.queries), collect(e.params) |
| 172 | + """ |
| 173 | + |
| 174 | + res = self.g.query(q, {'child_hash': child, 'parent_hash': parent}).result_set |
| 175 | + |
| 176 | + return (res[0][0], res[0][1]) |
| 177 | + |
0 commit comments