Home

ugit @main - refs - log -
-
https://git.jolheiser.com/ugit.git
The code powering this h*ckin' site
tree log patch
feat: rel images Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAmWpQAkACgkQuFOtpdp7 v3qp5g//Vupi0R4xwazDxfhxjfQY1UOKiqrRynvxdPZAFM4Nx/IV/mwuZ7f3maYJ 1gStcLY8YNXYuKUMxhTU+cdL1EPN6JgcGtJa5T+dealccUZRPZsGt/EFT1ie4Usx aUgyTbWAAvDdAEcvZ6axHxejsOZC7KM0XjKJayoxuhXf0fHyhe3n8Rn0zae52O7Q Es1ZAJNF/20K/gd6Oi3r4y2RYvrDxYvwUxNV2HLpvufZ1VBhkiypi+jEOMarw7Na /g9ayidpSTsUlTgk6gnqSFURW91s+O5w/jzB4ssa5RKAfWER/BjgbcBO8QT6uzuQ FG0dOMKhMhx9r/HqGM5zpKKFbwOd6+q2UzM+blFpMbwsoGJd3ThmhyWymSNU8LJh 66TKUhdq8Ff4IrNIj5+ts8dLage9menargeEnXe3WiVlUSOxXqE/Vx+kju8z3MPZ +j5F/N9hnk2znROg4ff1eAm6P+1jiOc3fjCdoOaDoO9fs5X+tQ8xhA7wpGrOBi8W HPYz/sA850HXjxXSL7HJBTJSzIaT3pmEP6vvNFG1pNPtRBPy+eGyt5HbhvC2Kcek C5YWcEJqSmqZuJ2JP18HWkGcwWMp2rb6jeCDHJF3+FDsNX4HdNQWIUcseL755RB2 8mZkMSVIuU4lDbnSigjHjS9t6pPfWA4ymkAycYqYO7jzjb1gBCI= =urlx -----END PGP SIGNATURE-----
jolheiser <john.olheiser@gmail.com>
1 year ago
6 changed files, 245 additions(+), 63 deletions(-)
README.mdflake.nixgo.modinternal/html/markdown.gointernal/html/repo_commit.templinternal/html/repo_commit_templ.go
M README.mdREADME.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
diff --git a/README.md b/README.md
index b526756dff25a1db2927a6e2ec653de0d97dcff9..7823d998c5e3dd740154138153142d5b21d54119 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # ugit
 
-<img style="width: 50px;" alt="ugit logo" src="/ugit/tree/main/assets/ugit.svg?raw&pretty"/>
+<img alt="ugit logo" style="width:50px;" src="./assets/ugit.svg" />
 
 Minimal git server
 
@@ -29,4 +29,4 @@ ## License
 
 [MIT](LICENSE)
 
-Lots of inspiration and some starting code used from [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE) and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license).
+Lots of inspiration and some starting code used from [gitea](https://github.com/go-gitea/gitea) [(MIT)](https://github.com/go-gitea/gitea/blob/eba9c0ce48c7d43910eb77db74c6648157663ceb/LICENSE), [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE), and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license).
M flake.nixflake.nix
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/flake.nix b/flake.nix
index f9c52d0f7d42103274b68ff8a46d7a2c022dd0e1..15612ff4af2cbcfca2fb495339da7f74e3532a49 100644
--- a/flake.nix
+++ b/flake.nix
@@ -31,7 +31,7 @@         name = pname;
         path = ./.;
       });
       subPackages = ["cmd/ugitd"];
-      vendorHash = "sha256-E4cwC6c0d+HvHldqGYiWdPEdS2fch6imvAXzxb2MMdY=";
+      vendorHash = "sha256-2vIccmJs6YitRndccQOnUuFZCIbwzi0NfRzbixaLVTo=";
       meta = with pkgs.lib; {
         description = "Minimal git server";
         homepage = "https://git.jolheiser.com/ugit";
M go.modgo.mod
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/go.mod b/go.mod
index 3af97fb05ebeb5960238f3b62b6af33eee6ebb08..28b39d6469c5a5cab4ef6859e77e1b11660633f6 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ 	github.com/peterbourgon/ff/v3 v3.4.0
 	github.com/yuin/goldmark v1.6.0
 	github.com/yuin/goldmark-emoji v1.0.2
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
+	golang.org/x/net v0.19.0
 )
 
 require (
@@ -47,7 +48,6 @@ 	github.com/skeema/knownhosts v1.2.1 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	golang.org/x/crypto v0.17.0 // indirect
 	golang.org/x/mod v0.14.0 // indirect
-	golang.org/x/net v0.19.0 // indirect
 	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/tools v0.16.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
M internal/html/markdown.gointernal/html/markdown.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
diff --git a/internal/html/markdown.go b/internal/html/markdown.go
index e109162de66398d42dc38e2d8442c37f28b3d7e4..c4c8d5fc9f6aa904eac08f8d403fd84f1f3570b8 100644
--- a/internal/html/markdown.go
+++ b/internal/html/markdown.go
@@ -2,7 +2,12 @@ package html
 
 import (
 	"bytes"
+	"fmt"
+	"golang.org/x/net/html"
+	"io"
+	"net/url"
 	"path/filepath"
+	"strings"
 
 	"go.jolheiser.com/ugit/internal/git"
 
@@ -10,17 +15,23 @@ 	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
 	"github.com/yuin/goldmark"
 	emoji "github.com/yuin/goldmark-emoji"
 	highlighting "github.com/yuin/goldmark-highlighting/v2"
+	"github.com/yuin/goldmark/ast"
 	"github.com/yuin/goldmark/extension"
 	"github.com/yuin/goldmark/parser"
 	goldmarkhtml "github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
 )
 
-var Markdown = goldmark.New(
+var markdown = goldmark.New(
 	goldmark.WithRendererOptions(
 		goldmarkhtml.WithUnsafe(),
 	),
 	goldmark.WithParserOptions(
 		parser.WithAutoHeadingID(),
+		parser.WithASTTransformers(
+			util.Prioritized(astTransformer{}, 100),
+		),
 	),
 	goldmark.WithExtensions(
 		extension.GFM,
@@ -48,11 +59,23 @@ 		}
 	}
 
 	if readme != "" {
+		ctx := parser.NewContext()
+		mdCtx := markdownContext{
+			repo: repo.Name(),
+			ref:  ref,
+			path: path,
+		}
+		ctx.Set(renderContextKey, mdCtx)
 		var buf bytes.Buffer
-		if err := Markdown.Convert([]byte(readme), &buf); err != nil {
+		if err := markdown.Convert([]byte(readme), &buf, parser.WithContext(ctx)); err != nil {
+			return "", err
+		}
+		var out bytes.Buffer
+		if err := postProcess(buf.String(), mdCtx, &out); err != nil {
 			return "", err
 		}
-		return buf.String(), nil
+
+		return out.String(), nil
 	}
 
 	for _, md := range []string{"README.txt", "README", "readme.txt", "readme"} {
@@ -64,3 +87,128 @@ 	}
 
 	return "", nil
 }
+
+var renderContextKey = parser.NewContextKey()
+
+type markdownContext struct {
+	repo string
+	ref  string
+	path string
+}
+
+type astTransformer struct{}
+
+func (a astTransformer) Transform(node *ast.Document, _ text.Reader, pc parser.Context) {
+	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+		if !entering {
+			return ast.WalkContinue, nil
+		}
+
+		ctx := pc.Get(renderContextKey).(markdownContext)
+
+		switch v := n.(type) {
+		case *ast.Image:
+			link := v.Destination
+			if len(link) > 0 && !bytes.HasPrefix(link, []byte("http")) {
+				v.Destination = []byte(resolveLink(ctx.repo, ctx.ref, ctx.path, string(link)) + "?raw&pretty")
+			}
+
+			parent := n.Parent()
+			if _, ok := parent.(*ast.Link); !ok && parent != nil {
+				next := n.NextSibling()
+				wrapper := ast.NewLink()
+				wrapper.Destination = v.Destination
+				wrapper.Title = v.Title
+				wrapper.SetAttributeString("target", []byte("_blank"))
+				img := ast.NewImage(ast.NewLink())
+				img.Destination = link
+				img.Title = v.Title
+				for _, attr := range v.Attributes() {
+					img.SetAttribute(attr.Name, attr.Value)
+				}
+				for child := v.FirstChild(); child != nil; {
+					nextChild := child.NextSibling()
+					img.AppendChild(img, child)
+					child = nextChild
+				}
+				wrapper.AppendChild(wrapper, img)
+				wrapper.SetNextSibling(next)
+				parent.ReplaceChild(parent, n, wrapper)
+				v.SetNextSibling(next)
+			}
+		case *ast.Link:
+			link := v.Destination
+			if len(link) > 0 && !bytes.HasPrefix(link, []byte("http")) && link[0] != '#' && !bytes.HasPrefix(link, []byte("mailto")) {
+				v.Destination = []byte(resolveLink(ctx.repo, ctx.ref, ctx.path, string(link)))
+			}
+		}
+
+		return ast.WalkContinue, nil
+	})
+}
+
+func postProcess(in string, ctx markdownContext, out io.Writer) error {
+	node, err := html.Parse(strings.NewReader("<html><body>" + in + "</body></html"))
+	if err != nil {
+		return err
+	}
+	if node.Type == html.DocumentNode {
+		node = node.FirstChild
+	}
+
+	process(ctx, node)
+
+	renderNodes := make([]*html.Node, 0)
+	if node.Data == "html" {
+		node = node.FirstChild
+		for node != nil && node.Data != "body" {
+			node = node.NextSibling
+		}
+	}
+	if node != nil {
+		if node.Data == "body" {
+			child := node.FirstChild
+			for child != nil {
+				renderNodes = append(renderNodes, child)
+				child = child.NextSibling
+			}
+		} else {
+			renderNodes = append(renderNodes, node)
+		}
+	}
+	for _, node := range renderNodes {
+		if err := html.Render(out, node); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func process(ctx markdownContext, node *html.Node) {
+	if node.Type == html.ElementNode && node.Data == "img" {
+		for i, attr := range node.Attr {
+			if attr.Key != "src" {
+				continue
+			}
+			if len(attr.Val) > 0 && !strings.HasPrefix(attr.Val, "http") && !strings.HasPrefix(attr.Val, "data:image/") {
+				attr.Val = resolveLink(ctx.repo, ctx.ref, ctx.path, attr.Val) + "?raw&pretty"
+			}
+			node.Attr[i] = attr
+		}
+	}
+	for n := node.FirstChild; n != nil; n = n.NextSibling {
+		process(ctx, n)
+	}
+}
+
+func resolveLink(repo, ref, path, link string) string {
+	baseURL, err := url.Parse(fmt.Sprintf("/%s/tree/%s/%s", repo, ref, path))
+	if err != nil {
+		return ""
+	}
+	linkURL, err := url.Parse(link)
+	if err != nil {
+		return ""
+	}
+	return baseURL.ResolveReference(linkURL).String()
+}
M internal/html/repo_commit.templinternal/html/repo_commit.templ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
diff --git a/internal/html/repo_commit.templ b/internal/html/repo_commit.templ
index 7f07f145081601e442a16b27556e1ce276f54949..bb95a59fae886baa92f3de71e39949ff5b0abe7d 100644
--- a/internal/html/repo_commit.templ
+++ b/internal/html/repo_commit.templ
@@ -21,7 +21,19 @@ 			<div title={ rcc.Commit.When.Format("01/02/2006 03:04:05 PM") }>{ humanize.Time(rcc.Commit.When) }</div>
 		</div>
 		<div class="text-text mt-5">{ fmt.Sprintf("%d changed files, %d additions(+), %d deletions(-)", rcc.Commit.Stats.Changed, rcc.Commit.Stats.Additions, rcc.Commit.Stats.Deletions) }</div>
 		for _, file := range rcc.Commit.Files {
-			<div class="text-text mt-5"><span class="text-text/80">{ string(file.Action[0]) }</span>{ " " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.From.Commit, file.From.Path)) }>{ file.From.Path }</a>{ " -> " }<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.To.Commit, file.To.Path)) }>{ file.To.Path }</a></div>
+			<div class="text-text mt-5">
+				<span class="text-text/80" title={ file.Action }>{ string(file.Action[0]) }</span>
+				{ " " }
+				if file.From.Path != "" {
+					<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.From.Commit, file.From.Path)) }>{ file.From.Path }</a>
+				}
+				if file.From.Path != "" && file.To.Path != "" {
+					{ " -> " }
+				}
+				if file.To.Path != "" {
+					<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.To.Commit, file.To.Path)) }>{ file.To.Path }</a>
+				}
+			</div>
 			<div class="whitespace-pre commit">@templ.Raw(file.Patch)</div>
 		}
 	}
M internal/html/repo_commit_templ.gointernal/html/repo_commit_templ.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
diff --git a/internal/html/repo_commit_templ.go b/internal/html/repo_commit_templ.go
index 8a48de28e800dc0136b43650eb499947f4eb50cb..49c39c56f407fc6bb805e9aa465764d74745604e 100644
--- a/internal/html/repo_commit_templ.go
+++ b/internal/html/repo_commit_templ.go
@@ -219,38 +219,11 @@ 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
 			for _, file := range rcc.Commit.Files {
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-text mt-5\"><span class=\"text-text/80\">")
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-text mt-5\"><span class=\"text-text/80\" title=\"")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var18 string
-				templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(string(file.Action[0]))
-				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 82}
-				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
-				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
-				}
-				var templ_7745c5c3_Var19 string
-				templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
-				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 96}
-				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
-				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
-				}
-				var templ_7745c5c3_Var20 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.From.Commit, file.From.Path))
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var20)))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(file.Action))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
@@ -258,51 +231,100 @@ 				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var21 string
-				templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(file.From.Path)
+				var templ_7745c5c3_Var18 string
+				templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(string(file.Action[0]))
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 320}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 24, Col: 77}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span> ")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var22 string
-				templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(" -> ")
+				var templ_7745c5c3_Var19 string
+				templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 334}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 25, Col: 9}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var23 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.To.Commit, file.To.Path))
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var23)))
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
-				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
+				if file.From.Path != "" {
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var20 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.From.Commit, file.From.Path))
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var20)))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var21 string
+					templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(file.From.Path)
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 27, Col: 227}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
 				}
-				var templ_7745c5c3_Var24 string
-				templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(file.To.Path)
-				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 23, Col: 552}
+				if file.From.Path != "" && file.To.Path != "" {
+					var templ_7745c5c3_Var22 string
+					templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(" -> ")
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 30, Col: 13}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
-				if templ_7745c5c3_Err != nil {
-					return templ_7745c5c3_Err
+				if file.To.Path != "" {
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var23 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.To.Commit, file.To.Path))
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var23)))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					var templ_7745c5c3_Var24 string
+					templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(file.To.Path)
+					if templ_7745c5c3_Err != nil {
+						return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_commit.templ`, Line: 33, Col: 221}
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
+					_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
+					if templ_7745c5c3_Err != nil {
+						return templ_7745c5c3_Err
+					}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div class=\"whitespace-pre commit\">")
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"whitespace-pre commit\">")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}