183 lines
5.1 KiB
Python
Executable File
183 lines
5.1 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
import subprocess
|
|
import sqlite3
|
|
import argparse
|
|
import sys
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
FIELDS = {
|
|
"ct": "timestamp",
|
|
"aN": "author",
|
|
"aE": "email",
|
|
"cN": "committer",
|
|
"cE": "committer_email",
|
|
"s": "subject",
|
|
"b": "body",
|
|
"N": "notes",
|
|
}
|
|
|
|
|
|
def read_properties() -> Dict[str, Dict[str, str]]:
|
|
output = {}
|
|
for field in FIELDS:
|
|
# Construct the log request
|
|
format_str = f"%H %{field}%x00"
|
|
|
|
# Get the results
|
|
repo_results = subprocess.run(
|
|
["git", "log", f"--format=format:{format_str}"],
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
submodule_results = subprocess.run(
|
|
[
|
|
"git",
|
|
"submodule",
|
|
"foreach",
|
|
"git",
|
|
"log",
|
|
f"--format=format:{format_str}",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
|
|
# Parse the results
|
|
all_results = repo_results + submodule_results
|
|
all_results = all_results.split("\x00")
|
|
for result in all_results:
|
|
if " " not in result or result == "":
|
|
continue
|
|
commit_hash, value = result.split(" ", 1)
|
|
if commit_hash.startswith("Entering"):
|
|
continue
|
|
if commit_hash.startswith("\n"):
|
|
commit_hash = commit_hash[1:]
|
|
if commit_hash not in output:
|
|
output[commit_hash] = {}
|
|
output[commit_hash][field] = value
|
|
|
|
return output
|
|
|
|
|
|
def create_table(cursor: sqlite3.Cursor) -> None:
|
|
sql = "CREATE TABLE IF NOT EXISTS commits (hash TEXT PRIMARY KEY, "
|
|
for field in FIELDS.values():
|
|
ty = "TEXT"
|
|
if field == "timestamp":
|
|
ty = "INTEGER"
|
|
if field == "hash":
|
|
ty = "TEXT PRIMARY KEY"
|
|
|
|
sql += f"{field} {ty}, "
|
|
sql = sql[:-2] + ")"
|
|
logger.debug(f"Creating table with SQL: {sql}")
|
|
cursor.execute(sql)
|
|
|
|
|
|
def main() -> int:
|
|
# Handle program arguments
|
|
ap = argparse.ArgumentParser(
|
|
prog="git-log-sqlite", description="Interact with the git log using SQL"
|
|
)
|
|
ap.add_argument(
|
|
"--dump",
|
|
help="Path to a sqlite3 database file to dump contents to. DELETES EXISTING FILE",
|
|
type=Path,
|
|
)
|
|
ap.add_argument(
|
|
"--interactive",
|
|
"-i",
|
|
help="Start an interactive SQL session",
|
|
action="store_true",
|
|
)
|
|
ap.add_argument("--query", "-q", help="Run a query and print the results")
|
|
ap.add_argument("--no-header", help="Do not print the header", action="store_true")
|
|
ap.add_argument("--mode", help="Set the mode for the sqlite3 command", default="table")
|
|
ap.add_argument(
|
|
"-v", "--verbose", help="Enable verbose logging", action="store_true"
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
format="%(levelname)s: %(message)s",
|
|
)
|
|
|
|
# Interactive mode and query mode are mutually exclusive
|
|
if args.interactive and args.query:
|
|
logger.error("Interactive mode and query mode are mutually exclusive")
|
|
return 1
|
|
|
|
# If the user didn't specify anything, print the help message
|
|
if not (args.interactive or args.query):
|
|
ap.print_help()
|
|
return 1
|
|
|
|
# Read the properties
|
|
commits = read_properties()
|
|
logger.debug(f"Read {len(commits)} commits")
|
|
|
|
# Open a connection to the database
|
|
if args.dump:
|
|
args.dump.parent.mkdir(parents=True, exist_ok=True)
|
|
args.dump.unlink(missing_ok=True)
|
|
conn = sqlite3.connect(args.dump if args.dump else ":memory:")
|
|
cursor = conn.cursor()
|
|
|
|
# Create a table to store the data
|
|
create_table(cursor)
|
|
|
|
# Insert the data into the table
|
|
rows = list(commits.items())
|
|
rows.sort(key=lambda x: x[1]["ct"])
|
|
for commit_hash, data in rows:
|
|
sql = "INSERT INTO commits VALUES (" + ",".join(["?"] * (len(FIELDS) + 1)) + ")"
|
|
values = [commit_hash] + [data.get(field, None) for field in FIELDS.keys()]
|
|
cursor.execute(sql, values)
|
|
|
|
# Commit the changes
|
|
conn.commit()
|
|
|
|
# If just dumping, we are done
|
|
if args.dump:
|
|
conn.close()
|
|
return 0
|
|
|
|
# Dump to a temp file
|
|
import tempfile
|
|
|
|
temp_file = Path(tempfile.mkstemp()[1])
|
|
temp_conn = sqlite3.connect(temp_file)
|
|
temp_conn.executescript("\n".join(conn.iterdump()))
|
|
temp_conn.commit()
|
|
conn.close()
|
|
|
|
# Build the base sqlite command
|
|
sqlite_cmd = ["sqlite3", "--cmd", f".mode {args.mode}"]
|
|
if not args.no_header:
|
|
sqlite_cmd.append("--cmd")
|
|
sqlite_cmd.append(".headers on")
|
|
|
|
# If running a query, do so
|
|
if args.query:
|
|
subprocess.run(sqlite_cmd + [temp_file, args.query])
|
|
|
|
# If running interactively, do so
|
|
if args.interactive:
|
|
subprocess.run(sqlite_cmd + ["--interactive", temp_file])
|
|
|
|
# Delete the temp file
|
|
temp_file.unlink()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|