Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
reop_commit: Fix immediate newlines after <pre>
package main
import (
"fmt"
"net/http"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"go.lindenii.runxiyu.org/lindenii-common/misc"
)
type usable_file_patch struct {
From diff.File
To diff.File
Chunks []diff.Chunk
Chunks []usable_chunk
}
type usable_chunk struct {
Operation diff.Operation
Content string
}
func handle_repo_commit(w http.ResponseWriter, r *http.Request, params map[string]any) {
group_name, repo_name, commit_id_specified_string := params["group_name"].(string), params["repo_name"].(string), params["commit_id"].(string)
repo, description, err := open_git_repo(r.Context(), group_name, repo_name)
if err != nil {
http.Error(w, "Error opening repo: "+err.Error(), http.StatusInternalServerError)
return
}
params["repo_description"] = description
commit_id_specified_string_without_suffix := strings.TrimSuffix(commit_id_specified_string, ".patch")
commit_id := plumbing.NewHash(commit_id_specified_string_without_suffix)
commit_object, err := repo.CommitObject(commit_id)
if err != nil {
http.Error(w, "Error getting commit object: "+err.Error(), http.StatusInternalServerError)
return
}
if commit_id_specified_string_without_suffix != commit_id_specified_string {
patch, err := format_patch_from_commit(commit_object)
if err != nil {
http.Error(w, "Error formatting patch: "+err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, patch)
return
}
commit_id_string := commit_object.Hash.String()
if commit_id_string != commit_id_specified_string {
http.Redirect(w, r, commit_id_string, http.StatusSeeOther)
return
}
params["commit_object"] = commit_object
params["commit_id"] = commit_id_string
parent_commit_hash, patch, err := get_patch_from_commit(commit_object)
if err != nil {
http.Error(w, "Error getting patch from commit: "+err.Error(), http.StatusInternalServerError)
return
}
params["parent_commit_hash"] = parent_commit_hash.String()
params["patch"] = patch
// TODO: Remove unnecessary context
// TODO: Prepend "+"/"-"/" " instead of solely distinguishing based on color
usable_file_patches := make([]usable_file_patch, 0)
for _, file_patch := range patch.FilePatches() {
from, to := file_patch.Files()
if from == nil {
from = fake_diff_file_null
}
if to == nil {
to = fake_diff_file_null
}
chunks := []usable_chunk{}
for _, chunk := range file_patch.Chunks() {
content := chunk.Content()
if len(content) > 0 && content[0] == '\n' {
content = "\n" + content
} // Horrible hack to fix how browsers newlines that immediately proceed <pre>
chunks = append(chunks, usable_chunk{
Operation: chunk.Type(),
Content: content,
})
}
usable_file_patch := usable_file_patch{
Chunks: file_patch.Chunks(),
Chunks: chunks,
From: from,
To: to,
}
usable_file_patches = append(usable_file_patches, usable_file_patch)
}
params["file_patches"] = usable_file_patches
render_template(w, "repo_commit", params)
}
type fake_diff_file struct {
hash plumbing.Hash
mode filemode.FileMode
path string
}
func (f fake_diff_file) Hash() plumbing.Hash {
return f.hash
}
func (f fake_diff_file) Mode() filemode.FileMode {
return f.mode
}
func (f fake_diff_file) Path() string {
return f.path
}
var fake_diff_file_null = fake_diff_file{
hash: plumbing.NewHash("0000000000000000000000000000000000000000"),
mode: misc.First_or_panic(filemode.New("100644")),
path: "",
}
/* Base styles and variables */
html {
font-family: sans-serif;
background-color: var(--background-color);
color: var(--text-color);
--background-color: hsl(0, 0%, 100%);
--text-color: hsl(0, 0%, 0%);
--link-color: hsl(320, 50%, 36%);
--light-text-color: hsl(0, 0%, 45%);
--darker-border-color: hsl(0, 0%, 72%);
--lighter-border-color: hsl(0, 0%, 85%);
--text-decoration-color: hsl(0, 0%, 72%);
--darker-box-background-color: hsl(0, 0%, 92%);
--lighter-box-background-color: hsl(0, 0%, 95%);
--primary-color: hsl(320, 50%, 36%);
--primary-color-contrast: hsl(320, 0%, 100%);
--danger-color: hsl(0, 50%, 36%);
--danger-color-contrast: hsl(0, 0%, 100%);
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
html {
--background-color: hsl(0, 0%, 0%);
--text-color: hsl(0, 0%, 100%);
--link-color: hsl(320, 50%, 76%);
--light-text-color: hsl(0, 0%, 78%);
--darker-border-color: hsl(0, 0%, 35%);
--lighter-border-color: hsl(0, 0%, 25%);
--text-decoration-color: hsl(0, 0%, 30%);
--darker-box-background-color: hsl(0, 0%, 20%);
--lighter-box-background-color: hsl(0, 0%, 15%);
}
}
/* Global layout */
body {
margin: 0;
}
html, code, pre {
font-size: 1rem; /* TODO: Not always correct */
}
/* Footer styles */
footer {
margin-top: 1rem;
margin-left: auto;
margin-right: auto;
display: block;
padding: 0 5px;
width: fit-content;
text-align: center;
color: var(--light-text-color);
}
footer a:link, footer a:visited {
color: inherit;
}
/* Padding containers */
.padding-wrapper {
margin: 1rem auto;
max-width: 60rem;
padding: 0 5px;
}
.padding {
padding: 0 5px;
}
/* Link styles */
a:link, a:visited {
text-decoration-color: var(--text-decoration-color);
color: var(--link-color);
}
/* Readme inline code styling */
#readme code:not(pre > code) {
background-color: var(--lighter-box-background-color);
border-radius: 2px;
padding: 2px;
}
/* Table styles */
table {
border: var(--lighter-border-color) solid 1px;
border-spacing: 0px;
border-collapse: collapse;
}
table.wide {
width: 100%;
}
td, th {
padding: 3px 5px;
border: var(--lighter-border-color) solid 1px;
}
th, thead, tfoot {
background-color: var(--lighter-box-background-color);
}
th[scope=row] {
text-align: left;
}
tr.title-row > th, th.title-row, .title-row {
background-color: var(--darker-box-background-color);
}
td > pre {
margin: 0;
}
td#readme > *:last-child {
margin-bottom: 0;
}
td#readme > *:first-child {
margin-top: 0;
}
/* Table misc and scrolling */
.commit-id {
font-family: monospace;
}
.scroll {
overflow-x: auto;
}
/* Toggle table controls */
.toggle-table-off, .toggle-table-on {
display: none;
}
.toggle-table-off + table > thead > tr > th, .toggle-table-on + table > thead > tr > th {
padding: 0;
}
.toggle-table-off + table > thead > tr > th > label, .toggle-table-on + table > thead > tr > th > label {
width: 100%;
display: inline-block;
padding: 3px 0;
cursor: pointer;
}
.toggle-table-off:checked + table > tbody {
display: none;
}
.toggle-table-on + table > tbody {
display: none;
}
.toggle-table-on:checked + table > tbody {
display: table-row-group;
}
/* Diff/chunk styles */
.chunk-unchanged {
color: grey;
}
.chunk-addition {
color: green;
background-color: green;
}
@media (prefers-color-scheme: dark) {
.chunk-addition {
color: lime;
background-color: lime;
}
}
.chunk-deletion {
color: red;
background-color: red;
}
.chunk-unknown {
color: yellow;
background-color: yellow;
}
pre.chunk {
margin-top: 0;
margin-bottom: 0;
}
/* Toggle content sections */
.toggle-on-wrapper {
border: var(--lighter-border-color) solid 1px;
}
.toggle-on-toggle {
display: none;
}
.toggle-on-header {
cursor: pointer;
display: block;
width: 100%;
background-color: var(--lighter-box-background-color);
}
.toggle-on-header > span {
padding: 3px 5px;
display: inline-block;
}
.toggle-on-content {
display: none;
}
.toggle-on-toggle:checked + .toggle-on-header + .toggle-on-content {
display: block;
}
/* File display styles */
.file-patch + .file-patch {
margin-top: 0.5rem;
}
.file-content {
padding: 3px 5px;
}
.file-header {
font-family: monospace;
}
/* Form elements */
textarea {
box-sizing: border-box;
background-color: var(--lighter-box-background-color);
resize: vertical;
}
textarea,
input[type=text],
input[type=password] {
font-family: sans-serif;
font-size: smaller;
background-color: var(--lighter-box-background-color);
color: var(--text-color);
border: none;
padding: 0.3rem;
width: 100%;
box-sizing: border-box;
}
td.tdinput, th.tdinput {
padding: 0;
}
td.tdinput textarea,
td.tdinput input[type=text],
td.tdinput input[type=password],
th.tdinput textarea,
th.tdinput input[type=text],
th.tdinput input[type=password] {
background-color: transparent;
}
/* Button styles */
.btn-primary {
background: var(--primary-color);
color: var(--primary-color-contrast);
border: var(--lighter-border-color) 1px solid;
font-weight: bold;
}
.btn-danger {
background: var(--danger-color);
color: var(--danger-color-contrast);
border: var(--lighter-border-color) 1px solid;
font-weight: bold;
}
.btn-white {
background: var(--primary-color-contrast);
color: var(--primary-color);
border: var(--lighter-border-color) 1px solid;
}
.btn-normal,
input[type=file]::file-selector-button {
background: var(--lighter-box-background-color);
border: var(--lighter-border-color) 1px solid !important;
color: var(--light-text-color);
}
.btn, .btn-white, .btn-danger, .btn-normal, .btn-primary,
input[type=submit],
input[type=file]::file-selector-button {
display: inline-block;
width: auto;
min-width: fit-content;
border-radius: 0;
padding: .1rem .75rem;
font-size: 0.9rem;
transition: background .1s linear;
cursor: pointer;
}
a.btn, a.btn-white, a.btn-danger, a.btn-normal, a.btn-primary {
text-decoration: none;
}
/* Header layout */
header#main-header {
background-color: var(--lighter-box-background-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
header#main-header > div#main-header-forge-title {
flex-grow: 1;
}
header#main-header > div#main-header-user {
display: flex;
align-items: center;
}
{{- define "repo_commit" -}}
<!DOCTYPE html>
<html lang="en">
<head>
{{ template "head_common" . }}
<title>{{ .group_name }}/repos/{{ .repo_name }} – {{ .global.forge_title }}</title>
</head>
<body class="repo-commit">
{{ template "header" . }}
<div class="padding-wrapper scroll">
<table id="commit-info-table">
<thead>
<tr class="title-row">
<th colspan="2">Commit info</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">ID</th>
<td>{{ .commit_id }}</td>
</tr>
<tr>
<th scope="row">Author</th>
<td>{{ .commit_object.Author.Name }} <<a href="mailto:{{ .commit_object.Author.Email }}">{{ .commit_object.Author.Email }}</a>></td>
</tr>
<tr>
<th scope="row">Author date</th>
<td>{{ .commit_object.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</td>
</tr>
<tr>
<th scope="row">Committer</th>
<td>{{ .commit_object.Committer.Name }} <<a href="mailto:{{ .commit_object.Committer.Email }}">{{ .commit_object.Committer.Email }}</a>></td>
</tr>
<tr>
<th scope="row">Committer date</th>
<td>{{ .commit_object.Committer.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</td>
</tr>
<tr>
<th scope="row">Message</th>
<td><pre>{{ .commit_object.Message }}</pre></td>
</tr>
<tr>
<th scope="row">Actions</th>
<td><pre><a href="{{ .commit_object.Hash }}.patch">Get patch</a></pre></td>
</tr>
</tbody>
</table>
</div>
<div class="padding-wrapper">
{{ $parent_commit_hash := .parent_commit_hash }}
{{ $commit_object := .commit_object }}
{{ range .file_patches }}
<div class="file-patch toggle-on-wrapper">
<input type="checkbox" id="toggle-{{ .From.Hash }}{{ .To.Hash }}" class="file-toggle toggle-on-toggle">
<label for="toggle-{{ .From.Hash }}{{ .To.Hash }}" class="file-header toggle-on-header">
<span>
{{ if eq .From.Path "" }}
--- /dev/null
{{ else }}
--- a/<a href="../tree/{{ .From.Path }}?commit={{ $parent_commit_hash }}">{{ .From.Path }}</a> {{ .From.Mode }}
{{ end }}
<br />
{{ if eq .To.Path "" }}
+++ /dev/null
{{ else }}
+++ b/<a href="../tree/{{ .To.Path }}?commit={{ $commit_object.Hash }}">{{ .To.Path }}</a> {{ .To.Mode }}
{{ end }}
</span>
</label>
<div class="file-content toggle-on-content scroll">
{{ range .Chunks }}
{{ if eq .Type 0 }}
{{ if eq .Operation 0 }}
<pre class="chunk chunk-unchanged">{{ .Content }}</pre>
{{ else if eq .Type 1 }}
{{ else if eq .Operation 1 }}
<pre class="chunk chunk-addition">{{ .Content }}</pre>
{{ else if eq .Type 2 }}
{{ else if eq .Operation 2 }}
<pre class="chunk chunk-deletion">{{ .Content }}</pre>
{{ else }}
<pre class="chunk chunk-unknown">{{ .Content }}</pre>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
</div>
<footer>
{{ template "footer" . }}
</footer>
</body>
</html>
{{- end -}}