From 56c30ed7bdcbcae5c2663c09971cc173222f8424 Mon Sep 17 00:00:00 2001 From: Luka Hietala Date: Wed, 19 Nov 2025 18:04:39 +0200 Subject: [PATCH] basic blame --- app.py | 7 +++++ git/blame.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ git/commit.py | 1 + templates/blame.html | 35 +++++++++++++++++++++++ templates/blob.html | 1 + 5 files changed, 110 insertions(+) create mode 100644 git/blame.py create mode 100644 templates/blame.html diff --git a/app.py b/app.py index 3b87e76..7823067 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ from git.tree import get_tree_items from git.blob import get_blob from git.misc import get_version from git.diff import get_diff +from git.blame import get_blame app = Flask(__name__) @@ -53,6 +54,12 @@ def repo_blob_path(repo_name, path): blob = get_blob(f"{repo_path}/{repo_name}", ref, path) return render_template("blob.html", repo_name=repo_name, ref=ref, path=path, blob=blob) +@app.route("//blame/") +def repo_blame_path(repo_name, path): + ref = request.args.get('ref', 'HEAD') + blame = get_blame(f"{repo_path}/{repo_name}", ref, path) + return render_template("blame.html", repo_name=repo_name, ref=ref, path=path, blame=blame) + @app.route("//diff") def repo_diff(repo_name): id1 = request.args.get('id1', 'HEAD') diff --git a/git/blame.py b/git/blame.py new file mode 100644 index 0000000..a230b63 --- /dev/null +++ b/git/blame.py @@ -0,0 +1,66 @@ +import pygit2 as git + +# discourage using blame because its very expensive, especially on repos with long commits history +# retrieves blame information for a file at given ref and path +def get_blame(repo_path, ref="HEAD", file_path=""): + repo = git.Repository(repo_path) + obj = repo.revparse_single(ref) + if obj.type == git.GIT_OBJECT_COMMIT: + commit = obj + else: + commit = obj.peel(git.GIT_OBJECT_COMMIT) + + # traverse to the blob path + # TODO: make this common across more modules + tree = commit.tree + blob = None + if file_path: + parts = file_path.rstrip('/').split('/') + for part in parts: + found = False + for entry in tree: + if entry.name == part: + if entry.type == git.GIT_OBJECT_BLOB: + blob = repo.get(entry.id) + found = True + break + elif entry.type == git.GIT_OBJECT_TREE: + tree = repo.get(entry.id) + found = True + break + if not found: + return None # path not found + if blob is None: + return None + + blame = repo.blame(file_path) + + # get blob content lines directly. maybe later use lines_in_hunk + content_lines = blob.data.decode('utf-8', errors='replace').splitlines() + + # create a list to hold blame info per line + blame_lines = [None] * len(content_lines) + for hunk in blame: + # https://libgit2.org/docs/reference/main/blame/git_blame_hunk.html + start = hunk.final_start_line_number - 1 # to 0 index, since using python lists + end = start + hunk.lines_in_hunk + commit = repo.get(hunk.final_commit_id) # last commit oid + # TODO: more info if needed + info = { + 'commit_id': str(hunk.final_commit_id), + 'author': commit.author, + } + # fill premade info for lines in this hunk + for i in range(start, min(end, len(blame_lines))): # prevent index overflow, with min + blame_lines[i] = info + + # combine content lines with their blame info + result = [] + for i, line in enumerate(content_lines): + result.append({ + 'line_num': i + 1, + 'content': line, + 'blame': blame_lines[i] + }) + + return result \ No newline at end of file diff --git a/git/commit.py b/git/commit.py index 1c13bf5..6627a7e 100644 --- a/git/commit.py +++ b/git/commit.py @@ -4,6 +4,7 @@ import pygit2 as git def get_commits(path, ref="HEAD", max_count=None, skip=0): repo = git.Repository(path) commits = [] + # TODO: accept blob oids to filter commits that touch specific blobs walker = repo.walk(repo.revparse_single(ref).id, git.GIT_SORT_TIME) n = 0 diff --git a/templates/blame.html b/templates/blame.html new file mode 100644 index 0000000..7987724 --- /dev/null +++ b/templates/blame.html @@ -0,0 +1,35 @@ +{% block content %} +

Blame: {{ path }}

+{% if blame %} + + + + + + + + + + + + {% for line in blame %} + + + {% if line.blame %} + + + + {% else %} + + + + {% endif %} + + + {% endfor %} + +
LineCommitAuthorDateContent
{{ line.line_num }}{{ line.blame.commit_id[:8] }}{{ line.blame.author.name }}{{ line.blame.author.time }}
{{ line.content }}
+{% else %} +

No blame info

+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/blob.html b/templates/blob.html index d80ae27..f8cd857 100644 --- a/templates/blob.html +++ b/templates/blob.html @@ -2,6 +2,7 @@

Blob: {{ blob.name }}

Blob id: {{ blob.id }}

Size: {{ blob.size }} bytes

+

Blame

Content:

{% if blob.is_binary %}
Binary...
-- 2.47.3