From 7c1d43a6417e6288419e825b3be0de58efee208a Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 14 Oct 2019 15:01:38 -0400 Subject: [PATCH] Implement send-email helper UI --- ...3caa0b_add_source_repo_id_to_repository.py | 25 ++ gitsrht/app.py | 2 + gitsrht/blueprints/api.py | 3 +- gitsrht/blueprints/email.py | 303 ++++++++++++++++++ gitsrht/blueprints/repo.py | 22 +- gitsrht/git.py | 22 +- gitsrht/repos.py | 15 + gitsrht/templates/commit.html | 68 +--- gitsrht/templates/send-email-end.html | 134 ++++++++ gitsrht/templates/send-email-review.html | 111 +++++++ gitsrht/templates/send-email.html | 131 ++++++++ gitsrht/templates/summary.html | 136 +++++--- gitsrht/templates/utils.html | 98 +++++- gitsrht/types/__init__.py | 1 + scss/main.scss | 89 +++++ 15 files changed, 1026 insertions(+), 134 deletions(-) create mode 100644 gitsrht/alembic/versions/1152333caa0b_add_source_repo_id_to_repository.py create mode 100644 gitsrht/blueprints/email.py create mode 100644 gitsrht/templates/send-email-end.html create mode 100644 gitsrht/templates/send-email-review.html create mode 100644 gitsrht/templates/send-email.html diff --git a/gitsrht/alembic/versions/1152333caa0b_add_source_repo_id_to_repository.py b/gitsrht/alembic/versions/1152333caa0b_add_source_repo_id_to_repository.py new file mode 100644 index 0000000..7785d8c --- /dev/null +++ b/gitsrht/alembic/versions/1152333caa0b_add_source_repo_id_to_repository.py @@ -0,0 +1,25 @@ +"""Add source_repo_id to Repository + +Revision ID: 1152333caa0b +Revises: ddca72f1b7e2 +Create Date: 2019-10-14 14:22:16.032157 + +""" + +# revision identifiers, used by Alembic. +revision = '1152333caa0b' +down_revision = 'ddca72f1b7e2' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('repository', sa.Column('source_repo_id', + sa.Integer, sa.ForeignKey('repository.id'))) + op.add_column('repository', sa.Column('upstream_uri', sa.Unicode)) + + +def downgrade(): + op.drop_column('repository', 'source_repo_id') + op.drop_column('repository', 'upstream_uri') diff --git a/gitsrht/app.py b/gitsrht/app.py index 37dc7a0..e9deb37 100644 --- a/gitsrht/app.py +++ b/gitsrht/app.py @@ -23,11 +23,13 @@ class GitApp(ScmSrhtFlask): repo_api=GitRepoApi(), oauth_service=oauth_service) from gitsrht.blueprints.api import data + from gitsrht.blueprints.email import mail from gitsrht.blueprints.internal import internal from gitsrht.blueprints.repo import repo from gitsrht.blueprints.stats import stats self.register_blueprint(data) + self.register_blueprint(mail) self.register_blueprint(internal) self.register_blueprint(repo) self.register_blueprint(stats) diff --git a/gitsrht/blueprints/api.py b/gitsrht/blueprints/api.py index 3b1c90a..fb7d887 100644 --- a/gitsrht/blueprints/api.py +++ b/gitsrht/blueprints/api.py @@ -3,8 +3,9 @@ import json import pygit2 from flask import Blueprint, current_app, request, send_file, abort from gitsrht.annotations import validate_annotation -from gitsrht.blueprints.repo import lookup_ref, get_log, collect_refs +from gitsrht.blueprints.repo import lookup_ref, collect_refs from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree +from gitsrht.git import get_log from gitsrht.webhooks import RepoWebhook from io import BytesIO from scmsrht.access import UserAccess diff --git a/gitsrht/blueprints/email.py b/gitsrht/blueprints/email.py new file mode 100644 index 0000000..91695d4 --- /dev/null +++ b/gitsrht/blueprints/email.py @@ -0,0 +1,303 @@ +import email +import mailbox +import pygit2 +import re +import smtplib +import subprocess +import sys +from email.policy import SMTPUTF8 +from email.utils import make_msgid, parseaddr +from flask import Blueprint, render_template, abort, request, url_for, session +from flask import redirect +from gitsrht.git import Repository as GitRepository, commit_time, diffstat +from gitsrht.git import get_log +from scmsrht.access import get_repo_or_redir +from srht.config import cfg, cfgi, cfgb +from srht.flask import loginrequired, current_user +from srht.validation import Validation +from tempfile import NamedTemporaryFile +from textwrap import TextWrapper + +mail = Blueprint('mail', __name__) + +smtp_host = cfg("mail", "smtp-host", default=None) +smtp_port = cfgi("mail", "smtp-port", default=None) +smtp_user = cfg("mail", "smtp-user", default=None) +smtp_password = cfg("mail", "smtp-password", default=None) +smtp_from = cfg("mail", "smtp-from", default=None) + +@mail.route("///send-email") +@loginrequired +def send_email_start(owner, repo): + owner, repo = get_repo_or_redir(owner, repo) + with GitRepository(repo.path) as git_repo: + ncommits = int(request.args.get("commits", default=8)) + if ncommits > 32: + ncommits = 32 + selected_branch = request.args.get("branch", default=None) + + branches = [( + branch, + git_repo.branches[branch], + git_repo.get(git_repo.branches[branch].target) + ) for branch in git_repo.branches.local] + default_branch = git_repo.default_branch().name + branches = sorted(branches, + key=lambda b: (b[0] == selected_branch, commit_time(b[2])), + reverse=True) + + commits = dict() + for branch in branches[:2]: + commits[branch[0]] = get_log(git_repo, + branch[2], commits_per_page=ncommits) + + return render_template("send-email.html", + view="send-email", owner=owner, repo=repo, + selected_branch=selected_branch, branches=branches, + commits=commits) + +@mail.route("///send-email/end", methods=["POST"]) +@loginrequired +def send_email_end(owner, repo): + owner, repo = get_repo_or_redir(owner, repo) + with GitRepository(repo.path) as git_repo: + valid = Validation(request) + branch = valid.require("branch") + commit = valid.require(f"commit-{branch}") + + branch = git_repo.branches[branch] + tip = git_repo.get(branch.target) + start = git_repo.get(commit) + + log = get_log(git_repo, tip, until=start) + diffs = list() + for commit in log: + try: + parent = git_repo.revparse_single(commit.oid.hex + "^") + diff = git_repo.diff(parent, commit) + except KeyError: + parent = None + diff = commit.tree.diff_to_tree(swap=True) + diff.find_similar(pygit2.GIT_DIFF_FIND_RENAMES) + diffs.append(diff) + + return render_template("send-email-end.html", + view="send-email", owner=owner, repo=repo, + commits=log, start=start, diffs=diffs, + diffstat=diffstat) + +commentary_re = re.compile(r""" +---\n +(?P + (\ .*\ +\|\ +\d+\ [-+]+\n)+ + \ \d+\ files?\ changed,.*\n + \n + diff\ --git +) +""", re.MULTILINE | re.VERBOSE) + +def prepare_patchset(repo, git_repo, cover_letter=None, extra_headers=False, + to=None, cc=None): + with NamedTemporaryFile() as ntf: + wrapper = TextWrapper( + expand_tabs=False, + replace_whitespace=False, + width=72, + drop_whitespace=True, + break_long_words=False) + + valid = Validation(request) + start_commit = valid.require("start_commit") + end_commit = valid.require("end_commit") + cover_letter_subject = valid.optional("cover_letter_subject") + if cover_letter is None: + cover_letter = valid.optional("cover_letter") + if not valid.ok: + return None + + outgoing_domain = cfg("git.sr.ht", "outgoing-domain") + args = [ + "git", + "--git-dir", repo.path, + "-c", f"user.name=~{current_user.username}", + "-c", f"user.email={current_user.username}@{outgoing_domain}", + "format-patch", + f"--from=~{current_user.username} <{current_user.username}@{outgoing_domain}>", + f"--subject-prefix=PATCH {repo.name}", + "--stdout", + ] + if cover_letter: + args += ["--cover-letter"] + args += [f"{start_commit}^..{end_commit}"] + print(args) + p = subprocess.run(args, timeout=30, + stdout=subprocess.PIPE, stderr=sys.stderr) + if p.returncode != 0: + abort(400) # TODO: Something more useful, I suppose. + + ntf.write(p.stdout) + ntf.flush() + + policy = SMTPUTF8.clone(max_line_length=998) + factory = lambda f: email.message_from_bytes(f.read(), policy=policy) + mbox = mailbox.mbox(ntf.name) + emails = list(mbox) + + if cover_letter: + subject = emails[0]["Subject"] + del emails[0]["Subject"] + emails[0]["Subject"] = (subject + .replace("*** SUBJECT HERE ***", cover_letter_subject)) + body = emails[0].get_payload(decode=True).decode() + cover_letter = "\n".join(wrapper.wrap(cover_letter)) + body = body.replace("*** BLURB HERE ***", cover_letter) + emails[0].set_payload(body) + + for i, email in enumerate(emails[(1 if cover_letter else 0):]): + commentary = valid.optional(f"commentary-{i}") + if not commentary: + continue + commentary = "\n".join(wrapper.wrap(commentary)) + body = email.get_payload(decode=True).decode() + body = commentary_re.sub(r"---\n" + commentary.replace( + "\\", r"\\") + r"\n\n\g", body, count=1) + email.set_payload(body) + + if extra_headers: + msgid = make_msgid().split("@") + for i, email in enumerate(emails): + email["Message-ID"] = f"{msgid[0]}-{i}@{msgid[1]}" + email["X-Mailer"] = "git.sr.ht" + if i != 0: + email["In-Reply-To"] = f"{msgid[0]}-{0}@{msgid[1]}" + if to: + email["To"] = to + if cc: + email["Cc"] = cc + + return emails + +@mail.route("///send-email/review", methods=["POST"]) +@loginrequired +def send_email_review(owner, repo): + owner, repo = get_repo_or_redir(owner, repo) + with GitRepository(repo.path) as git_repo: + valid = Validation(request) + start_commit = valid.require("start_commit") + end_commit = valid.require("end_commit") + cover_letter = valid.optional("cover_letter") + cover_letter_subject = valid.optional("cover_letter_subject") + if cover_letter and not cover_letter_subject: + valid.error("Cover letter subject is required.", + field="cover_letter_subject") + if cover_letter_subject and not cover_letter: + valid.error("Cover letter body is required.", field="cover_letter") + + default_branch = git_repo.default_branch() + tip = git_repo.get(default_branch.target) + readme = None + if "README.md" in tip.tree: + readme = "README.md" + elif "README" in tip.tree: + readme = "README" + + emails = prepare_patchset(repo, git_repo) + if not emails or not valid.ok: + tip = git_repo.get(end_commit) + start = git_repo.get(start_commit) + + log = get_log(git_repo, tip, until=start) + diffs = list() + for commit in log: + try: + parent = git_repo.revparse_single(commit.oid.hex + "^") + diff = git_repo.diff(parent, commit) + except KeyError: + parent = None + diff = commit.tree.diff_to_tree(swap=True) + diff.find_similar(pygit2.GIT_DIFF_FIND_RENAMES) + diffs.append(diff) + + return render_template("send-email-end.html", + view="send-email", owner=owner, repo=repo, + commits=log, start=start, diffs=diffs, + diffstat=diffstat, **valid.kwargs) + + session["cover_letter"] = cover_letter + return render_template("send-email-review.html", + view="send-email", owner=owner, repo=repo, + readme=readme, emails=emails, + start=git_repo.get(start_commit), + end=git_repo.get(end_commit), + cover_letter=bool(cover_letter), + cover_letter_subject=cover_letter_subject) + +@mail.route("///send-email/send", methods=["POST"]) +@loginrequired +def send_email_send(owner, repo): + owner, repo = get_repo_or_redir(owner, repo) + with GitRepository(repo.path) as git_repo: + valid = Validation(request) + start_commit = valid.require("start_commit") + end_commit = valid.require("end_commit") + cover_letter_subject = valid.optional("cover_letter_subject") + + to = valid.require("patchset_to", friendly_name="To") + cc = valid.optional("patchset_cc") + recipients = list() + + if to: + to_recipients = [parseaddr(r)[1] for r in to.split(",")] + valid.expect('' not in to_recipients, + "Invalid recipient.", field="patchset_to") + recipients += to_recipients + if cc: + cc_recipients = [parseaddr(r)[1] for r in cc.split(",")] + valid.expect('' not in cc_recipients, + "Invalid recipient.", field="patchset_cc") + recipients += cc_recipients + + if not valid.ok: + cover_letter = session.get("cover_letter") + emails = prepare_patchset(repo, git_repo, cover_letter=cover_letter) + + default_branch = git_repo.default_branch() + tip = git_repo.get(default_branch.target) + readme = None + if "README.md" in tip.tree: + readme = "README.md" + elif "README" in tip.tree: + readme = "README" + + return render_template("send-email-review.html", + view="send-email", owner=owner, repo=repo, + readme=readme, emails=emails, + start=git_repo.get(start_commit), + end=git_repo.get(end_commit), + cover_letter=bool(cover_letter), + **valid.kwargs) + + cover_letter = session.pop("cover_letter", None) + emails = prepare_patchset(repo, git_repo, + cover_letter=cover_letter, extra_headers=True, + to=to, cc=cc) + if not emails: + abort(400) # Should work by this point + + # TODO: Send emails asyncronously + smtp = smtplib.SMTP(smtp_host, smtp_port) + smtp.ehlo() + if smtp_user and smtp_password: + smtp.starttls() + smtp.login(smtp_user, smtp_password) + print("Sending to receipients", recipients) + for email in emails: + smtp.sendmail(smtp_user, recipients, + email.as_bytes(unixfrom=False)) + smtp.quit() + + # TODO: If we're connected to a lists.sr.ht address, link to their URL + # in the archives. + session["message"] = "Your patchset has been sent." + return redirect(url_for('repo.summary', + owner=repo.owner, repo=repo.name)) diff --git a/gitsrht/blueprints/repo.py b/gitsrht/blueprints/repo.py index a9bcbbd..3f0c532 100644 --- a/gitsrht/blueprints/repo.py +++ b/gitsrht/blueprints/repo.py @@ -6,18 +6,18 @@ import pygments import subprocess import sys from datetime import timedelta -from jinja2 import Markup from flask import Blueprint, render_template, abort, send_file, request -from flask import Response, url_for +from flask import Response, url_for, session from gitsrht.annotations import AnnotatedFormatter from gitsrht.editorconfig import EditorConfig from gitsrht.git import Repository as GitRepository, commit_time, annotate_tree -from gitsrht.git import diffstat +from gitsrht.git import diffstat, get_log from gitsrht.rss import generate_feed from io import BytesIO +from jinja2 import Markup from pygments import highlight -from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer from pygments.formatters import HtmlFormatter +from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer from scmsrht.access import get_repo, get_repo_or_redir from scmsrht.formatting import get_formatted_readme, get_highlighted_file from scmsrht.redis import redis @@ -97,10 +97,13 @@ def summary(owner, repo): if isinstance(tag[1], pygit2.Tag) or isinstance(tag[1], pygit2.Commit)] tags = sorted(tags, key=lambda c: commit_time(c[1]), reverse=True) latest_tag = tags[0] if len(tags) else None + + message = session.pop("message", None) return render_template("summary.html", view="summary", owner=owner, repo=repo, readme=readme, commits=commits, latest_tag=latest_tag, default_branch=default_branch, - is_annotated=lambda t: isinstance(t, pygit2.Tag)) + is_annotated=lambda t: isinstance(t, pygit2.Tag), + message=message) def lookup_ref(git_repo, ref, path): ref = ref or git_repo.default_branch().name[len("refs/heads/"):] @@ -278,15 +281,6 @@ def collect_refs(git_repo): refs[_ref.commit.id.hex].append(_ref) return refs -def get_log(git_repo, commit, commits_per_page=20): - commits = list() - for commit in git_repo.walk(commit.id, pygit2.GIT_SORT_TIME): - commits.append(commit) - if len(commits) >= commits_per_page + 1: - break - - return commits - @repo.route("///log", defaults={"ref": None, "path": ""}) @repo.route("///log/", defaults={"path": ""}) @repo.route("///log//") diff --git a/gitsrht/git.py b/gitsrht/git.py index e6808b9..d54f08e 100644 --- a/gitsrht/git.py +++ b/gitsrht/git.py @@ -26,6 +26,16 @@ def commit_time(commit): def _get_ref(repo, ref): return repo._get(ref) +def get_log(git_repo, commit, commits_per_page=20, until=None): + commits = list() + for commit in git_repo.walk(commit.id, pygit2.GIT_SORT_TIME): + commits.append(commit) + if until is not None and commit == until: + break + elif len(commits) >= commits_per_page + 1: + break + return commits + class Repository(GitRepository): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -140,12 +150,12 @@ def annotate_tree(repo, tree, commit): return [entry.fetch_blob() for entry in tree.values()] -def _diffstat_name(delta): +def _diffstat_name(delta, anchor): 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"" + + f"" + f"{escape(delta.old_file.path)}" + f"") # Based on git/diff.c @@ -164,8 +174,8 @@ def _diffstat_name(delta): f"}}") return f"{delta.old_file.path} => {delta.new_file.path}" -def _diffstat_line(delta, patch): - name = _diffstat_name(delta) +def _diffstat_line(delta, patch, anchor): + name = _diffstat_name(delta, anchor) change = "" if delta.status not in [ pygit2.GIT_DELTA_ADDED, @@ -179,12 +189,12 @@ def _diffstat_line(delta, patch): f"{filemode(delta.new_file.mode)}") return Markup(f"{delta.status_char()} {name}{change}\n") -def diffstat(diff): +def diffstat(diff, anchor=""): stat = Markup(f"""{diff.stats.files_changed} files changed, {diff.stats.insertions } insertions(+), {diff.stats.deletions } deletions(-)\n\n""") for delta, patch in zip(diff.deltas, diff): - stat += _diffstat_line(delta, patch) + stat += _diffstat_line(delta, patch, anchor) return stat diff --git a/gitsrht/repos.py b/gitsrht/repos.py index 6244753..054bf3a 100644 --- a/gitsrht/repos.py +++ b/gitsrht/repos.py @@ -34,3 +34,18 @@ class GitRepoApi(SimpleRepoApi): RepoWebhook.Subscription.query.filter( RepoWebhook.Subscription.repo_id == repo.id).delete() super().do_delete_repo(repo) + + def do_clone_repo(self, source, repo): + subprocess.run(["mkdir", "-p", repo.path], check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["git", "clone", "--bare", source, repo.path]) + subprocess.run(["git", "config", "srht.repo-id", str(repo.id)], check=True, + cwd=repo.path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["ln", "-s", + post_update, + os.path.join(repo.path, "hooks", "update") + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(["ln", "-s", + post_update, + os.path.join(repo.path, "hooks", "post-update") + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) diff --git a/gitsrht/templates/commit.html b/gitsrht/templates/commit.html index 0e81410..c1beed7 100644 --- a/gitsrht/templates/commit.html +++ b/gitsrht/templates/commit.html @@ -40,73 +40,7 @@
{{diffstat(diff)}}
- {# God, working with
 tags is such a fucking mess #}
-        {% for patch in diff %}
-        
{#
-          #}{{patch.delta.status_char()}} {% if parent %}{{patch.delta.old_file.path}}{#
-         #}{% endif %} => {#
-         #}{{patch.delta.new_file.path}}{#
-         #} +{{patch.line_stats[1]}}{#
-         #} -{{patch.line_stats[2]}}{%
-            if patch.delta.old_file.mode != patch.delta.new_file.mode %}{#
-          #}{#
-          #}{% endif %}
-
-
{% for hunk in patch.hunks %}
-{% set hunk_index = loop.index %}@@ {#
-#}{% if parent %}{{hunk.old_start}},{{hunk.old_lines}} {#
-#}{% endif %}{{hunk.new_start}},{{hunk.new_lines}} {#
-#}@@{% if hunk.old_start == 0 %}
-{% endif %}{% for line in hunk.lines
-%}{{line.origin}}{%
-  if loop.first and hunk.old_start != 0
-%}{{line.content.lstrip()}}{%
-  else
-%} {{line.content}}{%
-  endif
-%}{% endfor %}
-{% endfor %}
-
- {% endfor %} + {{utils.commit_diff(repo, commit, diff)}} diff --git a/gitsrht/templates/send-email-end.html b/gitsrht/templates/send-email-end.html new file mode 100644 index 0000000..03a308b --- /dev/null +++ b/gitsrht/templates/send-email-end.html @@ -0,0 +1,134 @@ +{% extends "layout.html" %} +{% import "utils.html" as utils with context %} +{% block title %} +Preparing patchset for {{repo.owner.canonical_name}}/{{repo.name}} - {{cfg("sr.ht", "site-name")}} git +{% endblock %} +{% block body %} + +
+ {{csrf_token()}} + Finalize the patchset + + You can prune too-recent commits now. You'll be able to review the final + patchset before it's sent in the next step. + +
+ + {% for c in commits %} + + + {% endfor %} + +
+ Add a cover letter + + The cover letter is used to describe the patchset as a whole. Add any + comments useful for the reviewers of this patch. It will be wrapped to + 72 columns. + +
+ + {{valid.summary("cover_letter_subject")}} +
+ + {{valid.summary("cover_letter")}} +
+ +
+
+ + {% for diff in diffs %} + {% set c = commits[loop.index-1] %} +
+

{{ trim_commit(c.message) }}

+
+ {{ utils.commit_event(repo, c, full_body=True, diff=diff) }} +
+ Add commentary + + Add details or caveats useful for reviewing or testing this commit. + This won't appear in the log once the patch is applied. It will be + wrapped to 72 columns. + + +
+
+ {{utils.commit_diff(repo, c, diff, + anchor=c.oid.hex + "-", target_blank=True)}} +
+ {% endfor %} + +
+
+
+ +
+{% endblock %} diff --git a/gitsrht/templates/send-email-review.html b/gitsrht/templates/send-email-review.html new file mode 100644 index 0000000..59ec0b4 --- /dev/null +++ b/gitsrht/templates/send-email-review.html @@ -0,0 +1,111 @@ +{% extends "layout.html" %} +{% import "utils.html" as utils with context %} +{% block title %} +Review patchset for {{repo.owner.canonical_name}}/{{repo.name}} - {{cfg("sr.ht", "site-name")}} git +{% endblock %} +{% block body %} + +
+

Review your patchset

+

+ The following emails are going to be sent on your behalf. To whom should + they be sent? +

+ {{csrf_token()}} + + + +
+
+
+ + + {{valid.summary('patchset_to')}} + + This is usually a mailing list, or the project maintainer(s). + {% if readme %} + Check {{readme}} for more info. + {% endif %} + +
+
+ + + {{valid.summary('patchset_cc')}} +
+
+
+
+ +
+
+
+
+{# TODO: highlight the diff? #} +
+
+

+ This is equivalent to the following + git send-email command: +

+ {# TODO: More concise send-email commands, e.g. use HEAD where appropriate #} +
git config format.subjectPrefix "{{repo.name}}" # Only necessary once
+git send-email {% if cover_letter %}--cover-letter {% endif %}{{start.short_id}}^..{{end.short_id}}
+
+
+ {% for email in emails %} +

{{email["Subject"]}}

+

+{%- for key, value in email.items() -%}
+{{key}}: {{value}}
+{% endfor %}
+{{email.get_payload(decode=True).decode()}}
+ {% endfor %} +
+
+{% endblock %} diff --git a/gitsrht/templates/send-email.html b/gitsrht/templates/send-email.html new file mode 100644 index 0000000..10250a5 --- /dev/null +++ b/gitsrht/templates/send-email.html @@ -0,0 +1,131 @@ +{% extends "layout.html" %} +{% import "utils.html" as utils with context %} +{% block title %} +Preparing patchset for {{repo.owner.canonical_name}}/{{repo.name}} - {{cfg("sr.ht", "site-name")}} git +{% endblock %} +{% block body %} + +
+ {{csrf_token()}} + Select a branch + + {% for branch in branches[:2] %} + + + {% endfor %} + + {% if any(branches[2:]) %} +
+ More branches + +
+ {% endif %} + + Select the first commit + + Choose the earliest commit which you want to include in the patchset. + You'll be able to trim commits off the top in the next step. + + {% for branch in branches[:2] %} +
+ {% if commits[branch[0]][-1].parents %} + {% set show_commits = commits[branch[0]][:-1] %} + {% else %} + {% set show_commits = commits[branch[0]] %} + {% endif %} + {% for c in show_commits[::-1] %} + + + {% endfor %} +
+
+ {% if commits[branch[0]][-1].parents and (len(commits[branch[0]])-1) < 32 %} + {# TODO: suggest request-pull for >32 commits (or less, tbh) #} + Add more commits {{icon("caret-right")}} + {% endif %} +
+
+ {% endfor %} + +
+{% endblock %} diff --git a/gitsrht/templates/summary.html b/gitsrht/templates/summary.html index dad1037..6659c55 100644 --- a/gitsrht/templates/summary.html +++ b/gitsrht/templates/summary.html @@ -12,6 +12,9 @@ {% endif %}
+ {% if message %} +
{{message}}
+ {% endif %}
@@ -22,54 +25,101 @@ {% endfor %}
-
-

refs

-
- {% if default_branch %} -
{{default_branch.name[len("refs/heads/"):]}}
-
- browse {{icon("caret-right")}} - log {{icon("caret-right")}} -
- {% endif %} - {% if latest_tag %} -
{{ latest_tag[0][len("refs/tags/"):] }}
-
- {% if is_annotated(latest_tag[1]) %} - release notes {{icon("caret-right")}} +
+
+
+

refs

+
+ {% if default_branch %} +
{{default_branch.name[len("refs/heads/"):]}}
+
+ browse {{icon("caret-right")}} + log {{icon("caret-right")}} +
+ {% endif %} + {% if latest_tag %} +
{{ latest_tag[0][len("refs/tags/"):] }}
+
+ {% if is_annotated(latest_tag[1]) %} + release notes {{icon("caret-right")}} + {% else %} + browse {{icon("caret-right")}} + .tar.gz {{icon("caret-right")}} + {% endif %} +
+ {% endif %} +
+
+
+

clone

+ {% with read_only, read_write = repo|clone_urls %} +
+
read-only
+
{{read_only}}
+
read/write
+
{{read_write}}
+
+ {% endwith %} +
+
+
+
+ {% if current_user == repo.owner %} + Prepare a patchset {{icon('caret-right')}} +

+ + Use this or git + send-email to send changes upstream. + +

+ {% elif current_user != repo.owner %} +
+ {{csrf_token()}} + + +

+ + You can also use your local clone with + git send-email. + +

+
{% else %} - browse {{icon("caret-right")}} - .tar.gz {{icon("caret-right")}} +

+ + You can contribute to this project without a + {{cfg('sr.ht', 'site-name')}} + account with + git send-email, + or you can sign up here. + +

{% endif %} -
- {% endif %} -
-
-
-

clone

- {% with read_only, read_write = repo|clone_urls %} -
-
read-only
-
{{read_only}}
-
read/write
-
{{read_write}}
-
- {% endwith %} +
+
{% if readme %} -
+
{{ readme }}
diff --git a/gitsrht/templates/utils.html b/gitsrht/templates/utils.html index 0e820db..9cab9fe 100644 --- a/gitsrht/templates/utils.html +++ b/gitsrht/templates/utils.html @@ -18,16 +18,23 @@ endif %}{% endfor %} {% endmacro %} {% macro commit_event(repo, c, - full_body=False, refs={}, full_id=False, - parents=False, skip_body=False) %} + full_body=False, refs={}, full_id=False, diff=None, href=None, + parents=False, skip_body=False, target_blank=False) %}
{% if full_id %} {{c.id.hex}} {% else %} {{c.id.hex[:8]}} {% endif %} — @@ -82,9 +89,94 @@ endif %}{% endfor %}
{% if not skip_body %} {% if full_body %} -
{{c.message}}
+
{{c.message}}
+{%- if diff %}
+{{diffstat(diff, anchor=c.oid.hex + "-")}}{% endif -%}
+
{% else %}
{{ trim_commit(c.message) }}
{% endif %} {% endif %} {% endmacro %} + +{% macro commit_diff(repo, commit, diff, anchor="", target_blank=False) %} +{# God, working with
 tags is such a fucking mess #}
+{% for patch in diff %}
+
{#
+  #}{{patch.delta.status_char()}} {% if parent %}{{patch.delta.old_file.path}}{#
+ #}{% endif %} => {#
+ #}{{patch.delta.new_file.path}}{#
+ #} +{{patch.line_stats[1]}}{#
+ #} -{{patch.line_stats[2]}}{%
+    if patch.delta.old_file.mode != patch.delta.new_file.mode %}{#
+  #}{#
+  #}{% endif %}
+
+
{% for hunk in patch.hunks %}
+{% set hunk_index = loop.index %}@@ {#
+#}{% if parent %}{{hunk.old_start}},{{hunk.old_lines}} {#
+#}{% endif %}{{hunk.new_start}},{{hunk.new_lines}} {#
+#}@@{% if hunk.old_start == 0 %}
+{% endif %}{% for line in hunk.lines
+%}{{line.origin}}{%
+  if loop.first and hunk.old_start != 0
+%}{{line.content.lstrip()}}{%
+  else
+%} {{line.content}}{%
+  endif
+%}{% endfor %}
+{% endfor %}
+
+{% endfor %} +{% endmacro %} diff --git a/gitsrht/types/__init__.py b/gitsrht/types/__init__.py index 390fb62..dc5f311 100644 --- a/gitsrht/types/__init__.py +++ b/gitsrht/types/__init__.py @@ -1,3 +1,4 @@ +import sqlalchemy as sa from srht.database import Base from srht.oauth import ExternalUserMixin, ExternalOAuthTokenMixin from scmsrht.repos import BaseAccessMixin, BaseRedirectMixin diff --git a/scss/main.scss b/scss/main.scss index cc086a8..447ab59 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -215,3 +215,92 @@ img { } } } + +.prepare-patchset { + legend { + font-weight: bold; + } + + label { + margin-right: 1rem; + cursor: pointer; + } + + details { + display: inline; + color: $gray-600; + + &[open] { + display: block; + color: $black; + + summary { + color: $black; + } + } + + ul { + list-style: none; + padding-left: 0; + } + + li { + margin-top: 1rem; + } + } + + .event-list { + display: flex; + flex-direction: column; + + &.reverse { + flex-direction: column-reverse; + } + + input[type="radio"] { + display: none; + } + + & > .commit-diff { + margin-top: 1rem; + order: -2; + } + + & > .form-controls { + order: -1; + margin-top: 1rem; + align-self: flex-end; + + &.last { + order: -3; + } + } + + & > details { + order: 0; + } + + & > .event { + order: 1; + display: block; + margin: 0.25rem 0; + + // Because the order is reversed + &:last-child { + margin: 0.25rem 0; + } + + &:first-child { + margin: 0; + } + } + + input[type="radio"]:checked ~ .event { + background: lighten($info, 50) !important; + } + + input[type="radio"]:checked + .event { + background: lighten($info, 45) !important; + } + } +} -- 2.38.4