Lindenii Project Forge
Login
Commit info
ID9c4f62a4a0705657404745c84d9ba9472d32def5
AuthorRunxi Yu<me@runxiyu.org>
Author dateMon, 10 Feb 2025 21:19:55 +0800
CommitterRunxi Yu<me@runxiyu.org>
Committer dateMon, 10 Feb 2025 21:19:55 +0800
Actions
Get patch
*: category -> group
package main

import (
	"errors"
	"path/filepath"
	"strings"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"go.lindenii.runxiyu.org/lindenii-common/misc"
)

func open_git_repo(category_name, repo_name string) (*git.Repository, error) {
	return git.PlainOpen(filepath.Join(config.Git.Root, category_name, repo_name+".git"))
func open_git_repo(group_name, repo_name string) (*git.Repository, error) {
	return git.PlainOpen(filepath.Join(config.Git.Root, group_name, repo_name+".git"))
}

func build_display_git_tree(tree *object.Tree) []display_git_tree_entry_t {
	display_git_tree := make([]display_git_tree_entry_t, 0)
	for _, entry := range tree.Entries {
		display_git_tree_entry := display_git_tree_entry_t{}
		os_mode, err := entry.Mode.ToOSFileMode()
		if err != nil {
			display_git_tree_entry.Mode = "x---"
		} else {
			display_git_tree_entry.Mode = os_mode.String()[:4]
		}
		display_git_tree_entry.Is_file = entry.Mode.IsFile()
		display_git_tree_entry.Size, err = tree.Size(entry.Name)
		if err != nil {
			display_git_tree_entry.Size = 0
		}
		display_git_tree_entry.Name = strings.TrimPrefix(entry.Name, "/")
		display_git_tree = append(display_git_tree, display_git_tree_entry)
	}
	return display_git_tree
}

var err_get_recent_commits = errors.New("Error getting recent commits:")

func get_recent_commits(repo *git.Repository, head_hash plumbing.Hash) (recent_commits []*object.Commit, err error) {
	commit_iter, err := repo.Log(&git.LogOptions{From: head_hash})
	if err != nil {
		err = misc.Wrap_one_error(err_get_recent_commits, err)
		return nil, err
	}
	recent_commits = make([]*object.Commit, 0)
	defer commit_iter.Close()
	for range 3 {
		this_recent_commit, err := commit_iter.Next()
		if err != nil {
			err = misc.Wrap_one_error(err_get_recent_commits, err)
			return nil, err
		}
		recent_commits = append(recent_commits, this_recent_commit)
	}
	return
}
package main

import (
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

func handle_category_repos(w http.ResponseWriter, r *http.Request) {
func handle_group_repos(w http.ResponseWriter, r *http.Request) {
	data := make(map[string]any)
	category_name := r.PathValue("category_name")
	data["category_name"] = category_name
	entries, err := os.ReadDir(filepath.Join(config.Git.Root, category_name))
	group_name := r.PathValue("group_name")
	data["group_name"] = group_name
	entries, err := os.ReadDir(filepath.Join(config.Git.Root, group_name))
	if err != nil {
		_, _ = w.Write([]byte("Error listing repos: " + err.Error()))
		return
	}

	repos := []string{}
	for _, entry := range entries {
		this_name := entry.Name()
		if strings.HasSuffix(this_name, ".git") {
			repos = append(repos, strings.TrimSuffix(this_name, ".git"))
		}
	}
	data["repos"] = repos

	err = templates.ExecuteTemplate(w, "category_repos", data)
	err = templates.ExecuteTemplate(w, "group_repos", data)
	if err != nil {
		_, _ = w.Write([]byte("Error rendering template: " + err.Error()))
		return
	}
}
package main

import (
	"net/http"
)

func handle_repo_index(w http.ResponseWriter, r *http.Request) {
	data := make(map[string]any)
	// TODO: Sanitize path values
	category_name, repo_name := r.PathValue("category_name"), r.PathValue("repo_name")
	data["category_name"], data["repo_name"] = category_name, repo_name
	repo, err := open_git_repo(category_name, repo_name)
	group_name, repo_name := r.PathValue("group_name"), r.PathValue("repo_name")
	data["group_name"], data["repo_name"] = group_name, repo_name
	repo, err := open_git_repo(group_name, repo_name)
	if err != nil {
		_, _ = w.Write([]byte("Error opening repo: " + err.Error()))
		return
	}
	head, err := repo.Head()
	if err != nil {
		_, _ = w.Write([]byte("Error getting repo HEAD: " + err.Error()))
		return
	}
	data["ref"] = head.Name().Short()
	head_hash := head.Hash()
	recent_commits, err := get_recent_commits(repo, head_hash)
	if err != nil {
		_, _ = w.Write([]byte("Error getting recent commits: " + err.Error()))
		return
	}
	data["commits"] = recent_commits
	commit_object, err := repo.CommitObject(head_hash)
	if err != nil {
		_, _ = w.Write([]byte("Error getting commit object: " + err.Error()))
		return
	}
	tree, err := commit_object.Tree()
	if err != nil {
		_, _ = w.Write([]byte("Error getting file tree: " + err.Error()))
		return
	}

	data["readme"] = render_readme_at_tree(tree)
	data["files"] = build_display_git_tree(tree)

	err = templates.ExecuteTemplate(w, "repo_index", data)
	if err != nil {
		_, _ = w.Write([]byte("Error rendering template: " + err.Error()))
		return
	}
}
package main

import (
	"bytes"
	"html/template"
	"net/http"
	"strings"
	"path"

	chroma_formatters_html "github.com/alecthomas/chroma/v2/formatters/html"
	chroma_lexers "github.com/alecthomas/chroma/v2/lexers"
	chroma_styles "github.com/alecthomas/chroma/v2/styles"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
)

func handle_repo_tree(w http.ResponseWriter, r *http.Request) {
	data := make(map[string]any)
	// TODO: Sanitize path values
	raw_path_spec := r.PathValue("rest")
	ref_name, category_name, repo_name, path_spec := r.PathValue("ref"), r.PathValue("category_name"), r.PathValue("repo_name"), strings.TrimSuffix(raw_path_spec, "/")
	data["ref"], data["category_name"], data["repo_name"], data["path_spec"] = ref_name, category_name, repo_name, path_spec
	repo, err := open_git_repo(category_name, repo_name)
	ref_name, group_name, repo_name, path_spec := r.PathValue("ref"), r.PathValue("group_name"), r.PathValue("repo_name"), strings.TrimSuffix(raw_path_spec, "/")
	data["ref"], data["group_name"], data["repo_name"], data["path_spec"] = ref_name, group_name, repo_name, path_spec
	repo, err := open_git_repo(group_name, repo_name)
	if err != nil {
		_, _ = w.Write([]byte("Error opening repo: " + err.Error()))
		return
	}
	ref, err := repo.Reference(plumbing.NewBranchReferenceName(ref_name), true)
	if err != nil {
		_, _ = w.Write([]byte("Error getting repo reference: " + err.Error()))
		return
	}
	ref_hash := ref.Hash()
	commit_object, err := repo.CommitObject(ref_hash)
	if err != nil {
		_, _ = w.Write([]byte("Error getting commit object: " + err.Error()))
		return
	}
	tree, err := commit_object.Tree()
	if err != nil {
		_, _ = w.Write([]byte("Error getting file tree: " + err.Error()))
		return
	}

	var target *object.Tree
	if path_spec == "" {
		target = tree
	} else {
		target, err = tree.Tree(path_spec)
		if err != nil {
			file, err := tree.File(path_spec)
			if err != nil {
				_, _ = w.Write([]byte("Error retrieving path: " + err.Error()))
				return
			}
			file_contents, err := file.Contents()
			if err != nil {
				_, _ = w.Write([]byte("Error reading file: " + err.Error()))
				return
			}
			lexer := chroma_lexers.Match(path_spec)
			if lexer == nil {
				lexer = chroma_lexers.Fallback
			}
			iterator, err := lexer.Tokenise(nil, file_contents)
			if err != nil {
				_, _ = w.Write([]byte("Error tokenizing code: " + err.Error()))
				return
			}
			var formatted_unencapsulated bytes.Buffer
			style := chroma_styles.Get("autumn")
			formatter := chroma_formatters_html.New(chroma_formatters_html.WithClasses(true), chroma_formatters_html.TabWidth(8))
			err = formatter.Format(&formatted_unencapsulated, style, iterator)
			if err != nil {
				_, _ = w.Write([]byte("Error formatting code: " + err.Error()))
				return
			}
			formatted_encapsulated := template.HTML(formatted_unencapsulated.Bytes())
			data["file_contents"] = formatted_encapsulated
	
			err = templates.ExecuteTemplate(w, "repo_tree_file", data)
			if err != nil {
				_, _ = w.Write([]byte("Error rendering template: " + err.Error()))
				return
			}
			return
		}
	}

	if raw_path_spec[len(raw_path_spec) - 1] != '/' {
		http.Redirect(w, r, path.Base(path_spec) + "/", http.StatusSeeOther)
		return
	}

	data["readme"] = render_readme_at_tree(target)
	data["files"] = build_display_git_tree(target)

	err = templates.ExecuteTemplate(w, "repo_tree_dir", data)
	if err != nil {
		_, _ = w.Write([]byte("Error rendering template: " + err.Error()))
		return
	}
}
package main

import (
	"flag"
	"net"
	"net/http"

	"go.lindenii.runxiyu.org/lindenii-common/clog"
)

func main() {
	config_path := flag.String(
		"config",
		"/etc/lindenii/forge.scfg",
		"path to configuration file",
	)
	flag.Parse()

	err := load_config(*config_path)
	if err != nil {
		clog.Fatal(1, "Loading configuration: "+err.Error())
	}

	err = load_templates()
	if err != nil {
		clog.Fatal(1, "Loading templates: "+err.Error())
	}

	err = serve_static()
	if err != nil {
		clog.Fatal(1, "Serving static: "+err.Error())
	}

	serve_source()

	http.HandleFunc("/{$}", handle_index)
	http.HandleFunc("/{category_name}/repos/{$}", handle_category_repos)
	http.HandleFunc("/{category_name}/repos/{repo_name}/{$}", handle_repo_index)
	http.HandleFunc("/{category_name}/repos/{repo_name}/tree/{ref}/{rest...}", handle_repo_tree)
	http.HandleFunc("/{group_name}/repos/{$}", handle_group_repos)
	http.HandleFunc("/{group_name}/repos/{repo_name}/{$}", handle_repo_index)
	http.HandleFunc("/{group_name}/repos/{repo_name}/tree/{ref}/{rest...}", handle_repo_tree)

	listener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr)
	if err != nil {
		clog.Fatal(1, "Listening: "+err.Error())
	}

	err = http.Serve(listener, nil)
	if err != nil {
		clog.Fatal(1, "Serving: "+err.Error())
	}
}
{{- define "category_repos" -}}
{{- define "group_repos" -}}
<!DOCTYPE html>
<html>
	<head>
		{{ template "head_common" . }}
		<title>Repos in {{ .category_name }} &ndash; Lindenii Forge</title>
		<title>Repos in {{ .group_name }} &ndash; Lindenii Forge</title>
	</head>
	<body class="category-index">
	<body class="group-index">
		<div class="padding-wrapper">
			<h1>
			Repos in {{ .category_name }}
			Repos in {{ .group_name }}
			</h1>
			<ul>
				{{- range .repos }}
					<li>
						<a href="{{ . }}/">{{ . }}</a>
					</li>
				{{- end }}
			</ul>
		</div>
	</body>
</html>
{{- end -}}
{{- define "repo_index" -}}
<!DOCTYPE html>
<html>
	<head>
		{{ template "head_common" . }}
		<title>{{ .category_name }}/repos/{{ .repo_name }} &ndash; Lindenii Forge</title>
		<title>{{ .group_name }}/repos/{{ .repo_name }} &ndash; Lindenii Forge</title>
	</head>
	<body class="repo-index">
		<div class="padding-wrapper">
			<table id="recent-commits">
				<thead>
					<tr class="title-row">
						<th colspan="4">Recent Commits</th>
					</tr>
					<tr>
						<th scope="col">ID</th>
						<th scope="col">Title</th>
						<th scope="col">Author</th>
						<th scope="col">Time</th>
					</tr>
				</thead>
				<tbody>
					{{- range .commits }}
						<tr>
							<td class="commit-id">{{ slice .Hash.String 0 8 }}</td>
							<td class="commit-title">{{ .Message | first_line }}</td>
							<td class="commit-author">
								<a class="email-name" href="mailto:{{ .Author.Email }}">{{ .Author.Name }}</a>
							</td>
							<td class="commit-time">
								{{ .Author.When.Format "2006-01-02 15:04:05 -0700" }}
							</td>
						</tr>
					{{- end }}
				</tbody>
			</table>
		</div>
		<div class="padding-wrapper">
			<table id="file-tree">
				<thead>
					<tr class="title-row">
						<th colspan="3">/ on {{ .ref }}</th>
					</tr>
					<tr>
						<th scope="col">Mode</th>
						<th scope="col">Name</th>
						<th scope="col">Size</th>
					</tr>
				</thead>
				<tbody>
					{{- $ref := .ref }}
					{{- range .files }}
						<tr>
							<td class="file-mode">{{ .Mode }}</td>
							<td class="file-name"><a href="tree/{{ $ref }}/{{ .Name }}">{{ .Name }}</a>{{ if not .Is_file }}/{{ end }}</td>
							<td class="file-size">{{ .Size }}</td>
						</tr>
					{{- end }}
				</tbody>
			</table>
		</div>
		<div class="padding-wrapper">
			<div id="refs">
			</div>
		</div>
		<div class="padding-wrapper">
			{{ if .readme }}
				<table id="readme">
					<thead>
						<tr class="title-row">
							<th>
								README.md
							</th>
						</tr>
					</thead>
					<tbody>
						<tr>
							<td>
								{{ .readme -}}
							</td>
						</tr>
					</tbody>
				</table>
			{{ end }}
		</div>
	</body>
</html>
{{- end -}}
{{- define "repo_tree_dir" -}}
<!DOCTYPE html>
<html>
	<head>
		{{ template "head_common" . }}
		<title>{{ .category_name }}/repos/{{ .repo_name }}/{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} &ndash; Lindenii Forge</title>
		<title>{{ .group_name }}/repos/{{ .repo_name }}/{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} &ndash; Lindenii Forge</title>
	</head>
	<body class="repo-tree-dir">
		<div class="padding-wrapper">
			<table id="file-tree">
				<thead>
					<tr class="title-row">
						<th colspan="3">
							/{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} on {{ .ref }}
						</th>
					</tr>
					<tr>
						<th scope="col">Mode</th>
						<th scope="col">Name</th>
						<th scope="col">Size</th>
					</tr>
				</thead>
				<tbody>
					{{- $path_spec := .path_spec }}
					{{- range .files }}
						<tr>
							<td class="file-mode">{{ .Mode }}</td>
							<td class="file-name"><a href="{{ .Name }}{{ if not .Is_file }}/{{ end }}">{{ .Name }}</a>{{ if not .Is_file }}/{{ end }}</td>
							<td class="file-size">{{ .Size }}</td>
						</tr>
					{{- end }}
				</tbody>
			</table>
		</div>
		<div class="padding-wrapper">
			<div id="refs">
			</div>
		</div>
		<div class="padding-wrapper">
			{{ if .readme }}
				<table id="readme">
					<thead>
						<tr class="title-row">
							<th>
								README.md
							</th>
						</tr>
					</thead>
					<tbody>
						<tr>
							<td>
								{{ .readme -}}
							</td>
						</tr>
					</tbody>
				</table>
			{{ end }}
		</div>
	</body>
</html>
{{- end -}}
{{- define "repo_tree_file" -}}
<!DOCTYPE html>
<html>
	<head>
		{{ template "head_common" . }}
		<link rel="stylesheet" href="/static/chroma.css" />
		<title>{{ .category_name }}/repos/{{ .repo_name }}/{{ .path_spec }} &ndash; Lindenii Forge</title>
		<title>{{ .group_name }}/repos/{{ .repo_name }}/{{ .path_spec }} &ndash; Lindenii Forge</title>
	</head>
	<body class="repo-tree-file">
		<p>
			/{{ .path_spec }}
		</p>
		{{ .file_contents }}
	</body>
</html>
{{- end -}}