~edwargix/git.sr.ht

342ae75e7852514dd143ed692259933c1d696a42 — наб 4 years ago 3afdd1a
Add blame view

Ref: ~sircmpwn/git.sr.ht#157
4 files changed, 221 insertions(+), 30 deletions(-)

M gitsrht/blueprints/repo.py
A gitsrht/templates/blame.html
M gitsrht/templates/blob.html
M scss/main.scss
M gitsrht/blueprints/repo.py => gitsrht/blueprints/repo.py +66 -26
@@ 6,7 6,7 @@ import pygments
import subprocess
import sys
from datetime import timedelta
from flask import Blueprint, render_template, abort, send_file, request
from flask import Blueprint, render_template, abort, current_app, send_file, request
from flask import Response, url_for, session, redirect
from gitsrht.editorconfig import EditorConfig
from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree


@@ 245,41 245,81 @@ def tree(owner, repo, ref, path):
        return render_template("tree.html", view="tree", owner=owner, repo=repo,
                ref=ref, commit=commit, tree=tree, path=path)

def resolve_blob(git_repo, ref, path):
    commit, ref, path = lookup_ref(git_repo, ref, path)
    if not isinstance(commit, pygit2.Commit):
        abort(404)

    blob = None
    entry = None
    tree = commit.tree
    orig_commit = commit
    path = path.split("/")
    for part in path:
        if part == "":
            continue
        if part not in tree:
            abort(404)
        entry = tree[part]
        etype = (entry.type_str
                if hasattr(entry, "type_str") else entry.type)
        if etype == "blob":
            tree = annotate_tree(git_repo, tree, commit)
            commit = next(e.commit for e in tree if e.name == entry.name)
            blob = git_repo.get(entry.id)
            break
        tree = git_repo.get(entry.id)

    if not blob:
        abort(404)

    return orig_commit, ref, path, blob, entry

@repo.route("/<owner>/<repo>/blob/<path:ref>/<path:path>")
def raw_blob(owner, repo, ref, path):
    owner, repo = get_repo_or_redir(owner, repo)
    with GitRepository(repo.path) as git_repo:
        commit, ref, path = lookup_ref(git_repo, ref, path)
        if not isinstance(commit, pygit2.Commit):
            abort(404)

        blob = None
        entry = None
        tree = commit.tree
        path = path.split("/")
        for part in path:
            if part == "":
                continue
            if part not in tree:
                abort(404)
            entry = tree[part]
            etype = (entry.type_str
                    if hasattr(entry, "type_str") else entry.type)
            if etype == "blob":
                tree = annotate_tree(git_repo, tree, commit)
                commit = next(e.commit for e in tree if e.name == entry.name)
                blob = git_repo.get(entry.id)
                break
            tree = git_repo.get(entry.id)

        if not blob:
            abort(404)
        orig_commit, ref, path, blob, entry = resolve_blob(git_repo, ref, path)

        return send_file(BytesIO(blob.data),
                as_attachment=blob.is_binary,
                attachment_filename=entry.name,
                mimetype="text/plain" if not blob.is_binary else None)

def _lookup_user(email, cache):
    if email not in cache:
        cache[email] = current_app.lookup_user(email)
    return cache[email]

def lookup_user():
    cache = {}
    return lambda email: _lookup_user(email, cache)

@repo.route("/<owner>/<repo>/blame/<path:ref>", defaults={"path": ""})
@repo.route("/<owner>/<repo>/blame/<ref>/<path:path>")
def blame(owner, repo, ref, path):
    owner, repo = get_repo_or_redir(owner, repo)
    with GitRepository(repo.path) as git_repo:
        orig_commit, ref, path, blob, entry = resolve_blob(git_repo, ref, path)
        if blob.is_binary:
            return redirect(url_for("repo.log",
                owner=repo.owner.canonical_name, repo=repo.name, ref=ref,
                path="/".join(path)))

        try:
            blame = git_repo.blame("/".join(path), newest_commit=orig_commit.oid)
        except KeyError as ke:  # Path not in the tree
            abort(404)
        except ValueError:
            # ValueError: object at path 'hubsrht/' is not of the asked-for type 3
            abort(400)

        return render_template("blame.html", view="blame", owner=owner,
                repo=repo, ref=ref, path=path, entry=entry, blob=blob,
                blame=blame, commit=orig_commit, highlight_file=_highlight_file,
                editorconfig=EditorConfig(git_repo, orig_commit.tree, path),
                lookup_user=lookup_user())

@repo.route("/<owner>/<repo>/archive/<path:ref>.tar.gz")
def archive(owner, repo, ref):
    owner, repo = get_repo_or_redir(owner, repo)

A gitsrht/templates/blame.html => gitsrht/templates/blame.html +117 -0
@@ 0,0 1,117 @@
{% extends "repo-full.html" %}
{% import "utils.html" as utils %}
{% block title %}
<title>{{repo.owner.canonical_name}}/{{repo.name}}: {{path_join(*path)}} blame - {{cfg("sr.ht", "site-name")}} git</title>
{% endblock %}
{% block head %}
<style>
pre {
  tab-size: {{editorconfig.tab_width()}}
}
pre, body {
  padding-bottom: 0;
  margin-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="header-extension" style="margin-bottom: 0;">
  <div class="blob container-fluid">
    <span>
      {{ utils.breadcrumb(ref, repo, path, path_join) }}
      <span class="text-muted" style="margin-left: 1rem">
        <span title="{{"{0:0o}".format(entry.filemode)}}">
          {{stat.filemode(entry.filemode)}}
        </span>
      </span>
      <span class="text-muted" style="margin-left: 1rem">
        <span title="{{ blob.size }} bytes">
          {{humanize.naturalsize(blob.size,
            binary=True).replace("Byte", "byte")}}
        </span>
      </span>
      <span class="text-muted" style="margin-left: 1rem">
        <a href="{{url_for("repo.raw_blob", owner=repo.owner.canonical_name,
            repo=repo.name, ref=ref, path=path_join(*path))}}">
          View raw
        </a>
      </span>
      <span class="text-muted" style="margin-left: 1rem">
        <a href="{{url_for("repo.tree", owner=repo.owner.canonical_name,
            repo=repo.name, ref=ref, path=path_join(*path))}}">
          Unblame
        </a>
      </span>
    </span>
    <div class="commit">
      <a
        href="{{url_for("repo.commit",
          owner=repo.owner.canonical_name,
          repo=repo.name,
          ref=ref)}}"
      >{{commit.id.hex[:8]}}</a> &mdash;
      {% set author_user = lookup_user(commit.author.email) %}
      {% if author_user %}
      <a href="{{url_for("public.user_index",
        username=author_user.username)}}">{{commit.author.name}}</a>
      {% else %}
      {{commit.author.name}}
      {% endif %}
      {{trim_commit(commit.message)}}
      <span class="text-muted">
        {{ commit_time(commit) | date }}
      </span>
    </div>
    <div class="clearfix"></div>
  </div>
</div>
<div class="container-fluid" style="padding-left: 0; padding-right: 0;">
  {#
    This row has some weird styles going on. This prevents the page from
    scrolling horizontally
  #}
  <div class="row" style="margin-right: 0;">
    <div class="col-md-12 code-view">
      <pre class="ruler"><span>{% for i in range(
        editorconfig.max_line_length()) %} {% endfor %}</span></pre>
      {% set lns = namespace(line_count=0) %}
      <pre class="blame-user">
        {%- for hunk in blame %}
          {%- set lns.line_count = lns.line_count + hunk.lines_in_hunk -%}
          {%- set final_user = lookup_user(hunk.final_committer.email) -%}
          <div class="hunk">{#
          #}<a href="{{url_for("repo.commit",
              owner=repo.owner.canonical_name, repo=repo.name,
              ref=hunk.final_commit_id.hex)}}"
            >{{hunk.final_commit_id.hex[:8]}}</a> {% if final_user -%}
            <a href="{{url_for("public.user_index", username=final_user.username)}}">{{hunk.final_committer.name}}</a>{% else -%}
            {{hunk.final_committer.name}}{% endif %} {{ "\n" * hunk.lines_in_hunk -}}
          </div>
        {%- endfor -%}
      </pre>
      <pre class="blame-time">
        {%- for hunk in blame -%}
          <div class="hunk">{#
          #}<a
              href="{{url_for("repo.blame", owner=repo.owner.canonical_name,
                repo=repo.name, ref=hunk.final_commit_id,
                path=path_join(*path))}}"
            >{{ commit_time(repo.git_repo[hunk.final_commit_id]) | date }}</a>{{ "\n" * hunk.lines_in_hunk -}}
          </div>
        {%- endfor -%}
      </pre>
      <pre class="lines">{% for l in range(lns.line_count) %}<a
        href="#L{{loop.index}}"
        id="L{{loop.index}}"
        >{{loop.index}}</a>
{% endfor %}</pre>
      {{ highlight_file(repo, ref, entry.name,
        blob.data, blob.id.hex, commit.id.hex) }}
    </div>
  </div>
</div>
{% endblock %}

{% block scripts %}
<script src="/static/linelight.js"></script>
{% endblock %}

M gitsrht/templates/blob.html => gitsrht/templates/blob.html +8 -0
@@ 36,6 36,14 @@ pre, body {
          View raw
        </a>
      </span>
      {% if not blob.is_binary %}
        <span class="text-muted" style="margin-left: 1rem">
          <a href="{{url_for("repo.blame", owner=repo.owner.canonical_name,
              repo=repo.name, ref=ref, path=path_join(*path))}}">
            Blame
          </a>
        </span>
      {% endif %}
    </span>
    {% if commit %}
    <div class="commit">

M scss/main.scss => scss/main.scss +30 -4
@@ 81,12 81,38 @@ dt {

.code-view {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-columns: auto auto auto 1fr;
  grid-template-rows: auto;

  .lines {
  .blame-user {
    grid-column-start: 1;
    grid-row-start: 1;
    background: #ddd;

    .hunk {
      padding-left: 0.5rem;
    }
  }

  .blame-time {
    grid-column-start: 2;
    grid-row-start: 1;
    background: #ddd;
    border-right: 1px solid #444;
    text-align: right;

    .hunk {
      padding-right: 0.5rem;
    }
  }

  .hunk:nth-child(2n) {
    background: #eee;
  }

  .lines {
    grid-column-start: 3;
    grid-row-start: 1;
    border-right: 1px solid #444;
    text-align: right;
    padding-left: 0.5rem;


@@ 106,7 132,7 @@ dt {
  }

  .highlight {
    grid-column-start: 2;
    grid-column-start: 4;
    grid-row-start: 1;
    padding-left: 1rem;
    background: transparent;


@@ 119,7 145,7 @@ dt {

  .ruler {
    background: transparent;
    grid-column-start: 2;
    grid-column-start: 4;
    grid-row-start: 1;
    display: block;
    padding-left: calc(1rem + 4px);