~edwargix/git.sr.ht

c6533c216aa402ad33ca0c9a8534836f8f13034d — Drew DeVault 6 years ago d40fe4e
Implement commit view
M gitsrht/blueprints/repo.py => gitsrht/blueprints/repo.py +52 -38
@@ 9,7 9,7 @@ from flask_login import current_user
from gitsrht.access import get_repo, has_access, UserAccess
from gitsrht.editorconfig import EditorConfig
from gitsrht.redis import redis
from gitsrht.git import CachedRepository, commit_time, annotate_tree
from gitsrht.git import CachedRepository, commit_time, annotate_tree, diffstat
from gitsrht.types import User, Repository
from io import BytesIO
from pygments import highlight


@@ 89,29 89,6 @@ def summary(owner, repo):
            clone_urls=clone_urls, latest_tag=latest_tag,
            default_branch=default_branch)

def resolve_ref(git_repo, ref):
    commit = None
    if ref is None:
        branch = git_repo.default_branch()
        ref = branch.name[len("refs/heads/"):]
        commit = git_repo.get(branch.target)
    else:
        if f"refs/heads/{ref}" in git_repo.references:
            branch = git_repo.references[f"refs/heads/{ref}"]
            commit = git_repo.get(branch.target)
        elif f"refs/tags/{ref}" in git_repo.references:
            _ref = git_repo.references[f"refs/tags/{ref}"]
            tag = git_repo.get(_ref.target)
            commit = git_repo.get(tag.target)
        else:
            try:
                ref = git_repo.get(ref)
            except:
                abort(404)
    if not commit:
        abort(404)
    return ref, commit

@repo.route("/<owner>/<repo>/tree", defaults={"ref": None, "path": ""})
@repo.route("/<owner>/<repo>/tree/<ref>", defaults={"path": ""})
@repo.route("/<owner>/<repo>/tree/<ref>/<path:path>")


@@ 122,7 99,10 @@ def tree(owner, repo, ref, path):
    if not has_access(repo, UserAccess.read):
        abort(401)
    git_repo = CachedRepository(repo.path)
    ref, commit = resolve_ref(git_repo, ref)
    ref = ref or git_repo.default_branch().name[len("refs/heads/"):]
    commit = git_repo.revparse_single(ref)
    if commit is pygit2.Tag:
        commit = git_repo.get(commit.target)

    tree = commit.tree
    editorconfig = EditorConfig(git_repo, tree, path)


@@ 165,7 145,10 @@ def raw_blob(owner, repo, ref, path):
    if not has_access(repo, UserAccess.read):
        abort(401)
    git_repo = CachedRepository(repo.path)
    ref, commit = resolve_ref(git_repo, ref)
    ref = ref or git_repo.default_branch().name[len("refs/heads/"):]
    commit = git_repo.revparse_single(ref)
    if commit is pygit2.Tag:
        commit = git_repo.get(commit.target)

    blob = None
    entry = None


@@ 198,7 181,10 @@ def archive(owner, repo, ref):
    if not has_access(repo, UserAccess.read):
        abort(401)
    git_repo = CachedRepository(repo.path)
    ref, commit = resolve_ref(git_repo, ref)
    ref = ref or git_repo.default_branch().name[len("refs/heads/"):]
    commit = git_repo.revparse_single(ref)
    if commit is pygit2.Tag:
        commit = git_repo.get(commit.target)

    path = f"/tmp/{commit.id.hex}.tar.gz"
    try:


@@ 253,6 239,17 @@ class _AnnotatedRef:
        else:
            self.type = None

def collect_refs(git_repo):
    refs = {}
    for _ref in git_repo.references:
        _ref = _AnnotatedRef(git_repo, git_repo.references[_ref])
        if not _ref.type:
            continue
        if _ref.commit.id.hex not in refs:
            refs[_ref.commit.id.hex] = []
        refs[_ref.commit.id.hex].append(_ref)
    return refs

@repo.route("/<owner>/<repo>/log", defaults={"ref": None, "path": ""})
@repo.route("/<owner>/<repo>/log/<ref>", defaults={"path": ""})
@repo.route("/<owner>/<repo>/log/<ref>/<path:path>")


@@ 263,17 260,11 @@ def log(owner, repo, ref, path):
    if not has_access(repo, UserAccess.read):
        abort(401)
    git_repo = CachedRepository(repo.path)
    ref, commit = resolve_ref(git_repo, ref)

    refs = {}
    for _ref in git_repo.references:
        _ref = _AnnotatedRef(git_repo, git_repo.references[_ref])
        if not _ref.type:
            continue
        print(_ref.commit.id.hex, _ref.name)
        if _ref.commit.id.hex not in refs:
            refs[_ref.commit.id.hex] = []
        refs[_ref.commit.id.hex].append(_ref)
    ref = ref or git_repo.default_branch().name[len("refs/heads/"):]
    commit = git_repo.revparse_single(ref)
    if commit is pygit2.Tag:
        commit = git_repo.get(commit.target)
    refs = collect_refs(git_repo)

    from_id = request.args.get("from")
    if from_id:


@@ 289,3 280,26 @@ def log(owner, repo, ref, path):
    return render_template("log.html", view="log",
            owner=owner, repo=repo, ref=ref, path=path,
            commits=commits, refs=refs)

@repo.route("/<owner>/<repo>/commit/<ref>")
def commit(owner, repo, ref):
    owner, repo = get_repo(owner, repo)
    if not repo:
        abort(404)
    if not has_access(repo, UserAccess.read):
        abort(401)
    git_repo = CachedRepository(repo.path)
    commit = git_repo.revparse_single(ref)
    if commit is pygit2.Tag:
        ref = git_repo.get(commit.target)
    try:
        parent = git_repo.revparse_single(ref + "^")
        diff = git_repo.diff(parent, ref)
    except KeyError:
        diff = ref.tree.diff_to_tree()
    diff.find_similar(pygit2.GIT_DIFF_FIND_RENAMES)
    refs = collect_refs(git_repo)
    return render_template("commit.html", view="log",
        owner=owner, repo=repo, ref=ref, refs=refs,
        commit=commit, parent=parent,
        diff=diff, diffstat=diffstat, pygit2=pygit2)

M gitsrht/git.py => gitsrht/git.py +51 -0
@@ 3,6 3,8 @@ from datetime import datetime, timedelta, timezone
from functools import lru_cache
from gitsrht.redis import redis
from pygit2 import Repository, Tag
from jinja2 import Markup, escape
from stat import filemode
import pygit2
import json



@@ 131,3 133,52 @@ def annotate_tree(repo, tree, commit):
    redis.setex(key, cache, timedelta(days=30))

    return [entry.fetch_blob() for entry in tree.values()]

def _diffstat_name(delta):
    if delta.status == pygit2.GIT_DELTA_DELETED:
        return Markup(escape(delta.old_file.path))
    if delta.old_file.path == delta.new_file.path:
        return Markup(
                f"<a href='#{escape(delta.old_file.path)}'>" +
                f"{escape(delta.old_file.path)}" +
                f"</a>")
    # Based on git/diff.c
    pfx_length = 0
    old_path = delta.old_file.path
    new_path = delta.new_file.path
    for i in range(max(len(old_path), len(new_path))):
        if i >= len(old_path) or i >= len(new_path):
            break
        if old_path[i] == '/':
            pfx_length = i + 1
    # TODO: detect common suffix
    if pfx_length != 0:
        return (f"{delta.old_file.path[:pfx_length]}{{" +
            f"{delta.old_file.path[pfx_length:]} =&gt; {delta.new_file.path[pfx_length:]}" +
            f"}}")
    return f"{delta.old_file.path} => {delta.new_file.path}"

def _diffstat_line(delta, patch):
    name = _diffstat_name(delta)
    change = ""
    if delta.status not in [
                pygit2.GIT_DELTA_ADDED,
                pygit2.GIT_DELTA_DELETED,
            ]:
        if delta.old_file.mode != delta.new_file.mode:
            change = Markup(
                f" <span title='{delta.old_file.mode}'>" +
                f"{filemode(delta.old_file.mode)}</span> => " +
                f"<span title='{delta.new_file.mode}'>" +
                f"{filemode(delta.new_file.mode)}</span>")
    return Markup(f"{delta.status_char()} {name}{change}\n")

def diffstat(diff):
    stat = Markup(f"""{diff.stats.files_changed} files changed, <strong
        class="text-success">{diff.stats.insertions
        }</strong> insertions(+), <strong
        class="text-danger">{diff.stats.deletions
        }</strong> deletions(-)\n\n""")
    for delta, patch in zip(diff.deltas, diff):
        stat += _diffstat_line(delta, patch)
    return stat

A gitsrht/templates/commit.html => gitsrht/templates/commit.html +100 -0
@@ 0,0 1,100 @@
{% extends "repo.html" %}
{% import "utils.html" as utils %}
{% block content %}
<div class="container">
  <div class="row">
    <div class="col-md-10">
      <div class="event-list">
        <div class="event">
          {{ utils.commit_event(repo, commit, commit_time, trim_commit,
              full_body=True, full_id=True, refs=refs, parents=True,
              any=any) }}
        </div>
      </div>
    </div>
    <div class="col-md-2">
      <a href="#" class="btn btn-primary btn-block">
        browse {{icon("caret-right")}}
      </a>
      <a href="#" class="btn btn-default btn-block">
        patch {{icon("caret-right")}}
      </a>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <div class="event-list">
        <div class="event">
          <pre>{{diffstat(diff)}}</pre>
        </div>
        {# God, working with <pre> tags is such a fucking mess #}
        {% for patch in diff %}
        <pre style="margin-bottom: 0;"
        >{#
          #}{{patch.delta.status_char()}} {% if parent %}<a
           href="{{url_for("repo.tree",
              owner=repo.owner.canonical_name,
              repo=repo.name,
              ref=parent.id.hex,
              path=patch.delta.old_file.path)}}"
           id="{{patch.delta.old_file.path}}"
         >{{patch.delta.old_file.path}}</a>{#
         #}{% endif %} =&gt; {#
         #}<a
           href="{{url_for("repo.tree",
              owner=repo.owner.canonical_name,
              repo=repo.name,
              ref=commit.id.hex,
              path=patch.delta.new_file.path)}}"
           id="{{patch.delta.new_file.path}}"
         >{{patch.delta.new_file.path}}</a>{#
         #} <span class="pull-right"><span class="text-success">+{{patch.line_stats[1]}}</span>{#
         #} <span class="text-danger">-{{patch.line_stats[2]}}</span></span>{%
            if patch.delta.old_file.mode != patch.delta.new_file.mode %}{#
          #}{#
          #}{% endif %}</pre>
        <div class="event diff">
          <pre>{% for hunk in patch.hunks %}
{% set hunk_index = loop.index %}<strong
  class="text-info"
>@@ {#
#}{% if parent %}<a
  style="text-decoration: underline"
  href="{{url_for("repo.tree",
    owner=repo.owner.canonical_name,
    repo=repo.name,
    ref=parent.id.hex,
    path=patch.delta.old_file.path)}}#L{{hunk.old_start}}"
>{{hunk.old_start}}</a>,{{hunk.old_lines}} {#
#}{% endif %}<a
  style="text-decoration: underline"
  href="{{url_for("repo.tree",
    owner=repo.owner.canonical_name,
    repo=repo.name,
    ref=commit.id.hex,
    path=patch.delta.new_file.path)}}#L{{hunk.new_start}}"
>{{hunk.new_start}}</a>,{{hunk.new_lines}} {#
#}@@</strong
>{% if hunk.old_start == 0 %}
{% endif %}{% for line in hunk.lines
%}<span class="{{({
  "+":"text-success",
  "-":"text-danger",
  }).get(line.origin) or ""}}"><a
    href="#{{patch.delta.old_file.path}}-{{hunk_index}}-{{loop.index}}"
    id="{{patch.delta.old_file.path}}-{{hunk_index}}-{{loop.index}}"
    style="color: inherit"
>{{line.origin}}</a>{%
  if loop.first and hunk.old_start != 0
%}{{line.content.lstrip()}}{%
  else
%} {{line.content}}{%
  endif
%}</span>{% endfor %}
{% endfor %}</pre>
        </div>
        {% endfor %}
      </div>
    </div>
</div>
{% endblock %}

M gitsrht/templates/log.html => gitsrht/templates/log.html +3 -1
@@ 6,7 6,9 @@
    <div class="col-md-12">
      <div class="event-list">
        {% for c in commits[:-1] %}
        {{ utils.commit_event(repo, c, commit_time, None, True, refs) }}
        <div class="event">
          {{ utils.commit_event(repo, c, commit_time, None, True, refs) }}
        </div>
        {% endfor %}
      </div>
      <a

M gitsrht/templates/summary.html => gitsrht/templates/summary.html +3 -1
@@ 13,7 13,9 @@
    <div class="col-md-6">
      <div class="event-list" style="margin-bottom: 0.5rem">
        {% for c in commits %}
        {{ utils.commit_event(repo, c, commit_time, trim_commit) }}
        <div class="event">
          {{ utils.commit_event(repo, c, commit_time, trim_commit) }}
        </div>
        {% endfor %}
      </div>
    </div>

M gitsrht/templates/utils.html => gitsrht/templates/utils.html +55 -35
@@ 17,43 17,63 @@
endif %}{% endfor %}
{% endmacro %}

{% macro commit_event(repo, c, commit_time, trim_commit, full_body=False, refs={}) %}
<div class="event">
  <div>
    <a
      href="#"
      title="{{c.id.hex}}"
    >{{c.id.hex[:8]}}</a> &mdash;
    <a href="#">{{c.author.name}}</a>
    <a
      id="log-{{c.id}}"
      href="#log-{{c.id}}"
      class="text-muted pull-right"
    >{{ commit_time(c) | date }}</a>
    {% if c.id.hex in refs %}
    {% for ref in refs[c.id.hex] %}
    <a
      class="ref {{ref.type}}
        {{"annotated" if ref.type == "tag" and ref.tag.message else ""}}"
      {% if ref.type == "branch" %}
      href="{{url_for("repo.tree",
        owner=repo.owner.canonical_name, repo=repo.name, ref=ref.name)}}"
      {% else %}
      {# TODO: Annotated tag page #}
      href="#"
      {% endif %}
    >{{ref.name}}</a>
{% macro commit_event(
  repo, c, commit_time, trim_commit,
  full_body=False, refs={}, full_id=False, parents=False,
  any=None) %}
<div>
  {% if full_id %}
  {{c.id.hex}}
  {% else %}
  <a
    href="{{url_for("repo.commit", owner=repo.owner.canonical_name,
      repo=repo.name, ref=c.id.hex)}}"
    title="{{c.id.hex}}"
  >{{c.id.hex[:8]}}</a>
  {% endif %}
  &mdash;
  <a href="#">{{c.author.name}}</a>
  <a
    id="log-{{c.id}}"
    href="#log-{{c.id}}"
    class="text-muted pull-right"
  >{{ commit_time(c) | date }}</a>

  {% if parents and any(c.parents) %}
  <span style="margin-left: 0.5rem">
    {{icon('code-branch', cls="sm")}}
    {% for parent in c.parents %}
    <a href="{{url_for("repo.commit",
      owner=repo.owner.canonical_name,
      repo=repo.name,
      ref=parent.id.hex)}}"
    >{{parent.short_id}}</a>
    {% if not loop.last %}
    +
    {% endif %}
    {% endfor %}
  </span>
  {% endif %}

  {% if c.id.hex in refs %}
  {% for ref in refs[c.id.hex] %}
  <a
    class="ref {{ref.type}}
      {{"annotated" if ref.type == "tag" and ref.tag.message else ""}}"
    {% if ref.type == "branch" %}
    href="{{url_for("repo.tree",
      owner=repo.owner.canonical_name, repo=repo.name, ref=ref.name)}}"
    {% else %}
    {# TODO: Annotated tag page #}
    href="#"
    {% endif %}
  </div>
  {% if full_body %}
  <pre
    style="padding-left: 0; padding-right: 0; background: transparent"
  >{{c.message}}</pre>
  {% else %}
  <pre
    style="padding-left: 0; padding-right: 0; background: transparent"
  >{{ trim_commit(c.message) }}</pre>
  >{{ref.name}}</a>
  {% endfor %}
  {% endif %}
</div>
{% if full_body %}
<pre>{{c.message}}</pre>
{% else %}
<pre>{{ trim_commit(c.message) }}</pre>
{% endif %}
{% endmacro %}

M scss/main.scss => scss/main.scss +20 -1
@@ 16,6 16,12 @@
  background: #ddd;
}

pre {
  padding-left: 0;
  padding-right: 0;
  background: transparent;
}

.tree-list {
  display: grid;
  // mode name


@@ 72,6 78,9 @@
    grid-row-start: 1;
    border-right: 1px solid #444;
    text-align: right;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    background: #eee;
  }

  .highlight {


@@ 110,7 119,7 @@
.ref {
  border-width: 1px;
  border-style: solid;
  padding: 0.2rem;
  padding: 0.1rem 0.2rem;

  &.branch {
    border-color: darken($info, 20);


@@ 130,3 139,13 @@
    color: $white;
  }
}

.diff {
  .text-danger {
    color: darken($danger, 10) !important;
  }

  .text-success {
    color: darken($success, 10) !important;
  }
}