Add web commit history views
This commit is contained in:
@@ -24,7 +24,7 @@ Open the minimal web UI at the server URL, for example:
|
|||||||
http://localhost:8080/repos
|
http://localhost:8080/repos
|
||||||
```
|
```
|
||||||
|
|
||||||
Web UI routes include `/repos`, `/login`, `/register`, `/repos/new`, `/{username}`, `/{username}/{repo}`, `/{username}/{repo}/branches`, and pull request pages.
|
Web UI routes include `/repos`, `/login`, `/register`, `/repos/new`, `/{username}`, `/{username}/{repo}`, `/{username}/{repo}/branches`, `/{username}/{repo}/commits`, `/{username}/{repo}/commit`, and pull request pages.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ CLI auth config defaults to:
|
|||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
||||||
The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI.
|
The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, selected-branch latest commit and commit count status, commit history pages, single commit pages with limited diff rendering, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+276
-12
@@ -28,6 +28,7 @@ const (
|
|||||||
webAuthCookie = "gitocean_web_token"
|
webAuthCookie = "gitocean_web_token"
|
||||||
webCSRFCookie = "gitocean_csrf"
|
webCSRFCookie = "gitocean_csrf"
|
||||||
maxWebFileBytes = 1024 * 1024
|
maxWebFileBytes = 1024 * 1024
|
||||||
|
maxWebDiffBytes = 200 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
type webPageData struct {
|
type webPageData struct {
|
||||||
@@ -46,16 +47,18 @@ type webReposData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type webRepoData struct {
|
type webRepoData struct {
|
||||||
Repo Repository
|
Repo Repository
|
||||||
Ref string
|
Ref string
|
||||||
Path string
|
Path string
|
||||||
Entries []webTreeEntry
|
Entries []webTreeEntry
|
||||||
CanWrite bool
|
CanWrite bool
|
||||||
CanFork bool
|
CanFork bool
|
||||||
Branches []RefInfo
|
Branches []RefInfo
|
||||||
CloneURL string
|
CloneURL string
|
||||||
ParentPath string
|
ParentPath string
|
||||||
PRs []PullRequest
|
PRs []PullRequest
|
||||||
|
LatestCommit *webCommit
|
||||||
|
CommitCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
type webBlobData struct {
|
type webBlobData struct {
|
||||||
@@ -91,6 +94,30 @@ type webBranchesData struct {
|
|||||||
CanWrite bool
|
CanWrite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type webCommit struct {
|
||||||
|
Hash string
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
AuthorName string
|
||||||
|
AuthorEmail string
|
||||||
|
Date time.Time
|
||||||
|
Diff string
|
||||||
|
DiffTruncated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type webCommitHistoryData struct {
|
||||||
|
Repo Repository
|
||||||
|
Ref string
|
||||||
|
Branches []RefInfo
|
||||||
|
Commits []webCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
type webCommitData struct {
|
||||||
|
Repo Repository
|
||||||
|
Ref string
|
||||||
|
Commit webCommit
|
||||||
|
}
|
||||||
|
|
||||||
type webPRListData struct {
|
type webPRListData struct {
|
||||||
Repo Repository
|
Repo Repository
|
||||||
PRs []PullRequest
|
PRs []PullRequest
|
||||||
@@ -115,7 +142,9 @@ type webProfileData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
|
var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
|
||||||
"urlquery": url.QueryEscape,
|
"urlquery": url.QueryEscape,
|
||||||
|
"shortHash": shortHash,
|
||||||
|
"truncate": truncateString,
|
||||||
}).Parse(`{{define "layout"}}<!doctype html>
|
}).Parse(`{{define "layout"}}<!doctype html>
|
||||||
<html><head><meta charset="utf-8"><title>{{.Title}} - gitocean</title></head>
|
<html><head><meta charset="utf-8"><title>{{.Title}} - gitocean</title></head>
|
||||||
<body>
|
<body>
|
||||||
@@ -200,6 +229,10 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
|
|||||||
<input type="hidden" name="path" value="{{$d.Path}}">
|
<input type="hidden" name="path" value="{{$d.Path}}">
|
||||||
<button type="submit">switch</button>
|
<button type="submit">switch</button>
|
||||||
</form>
|
</form>
|
||||||
|
{{if $d.LatestCommit}}
|
||||||
|
<p>Latest commit on {{$d.Ref}}: <a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/commit?ref={{urlquery $d.Ref}}&id={{urlquery $d.LatestCommit.Hash}}"><code>{{shortHash $d.LatestCommit.Hash}}</code> {{truncate $d.LatestCommit.Subject 72}}</a></p>
|
||||||
|
{{end}}
|
||||||
|
<p><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/commits?ref={{urlquery $d.Ref}}">{{$d.CommitCount}} commits</a> on {{$d.Ref}}</p>
|
||||||
{{if $d.CanWrite}}
|
{{if $d.CanWrite}}
|
||||||
<p><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/edit?ref={{urlquery $d.Ref}}">create/edit file</a></p>
|
<p><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/edit?ref={{urlquery $d.Ref}}">create/edit file</a></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -245,6 +278,44 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "Commit History"}}
|
||||||
|
{{$d := .Data}}
|
||||||
|
<h1>Commits for {{$d.Repo.Owner}}/{{$d.Repo.Name}}</h1>
|
||||||
|
<p><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/tree?ref={{urlquery $d.Ref}}">repo</a></p>
|
||||||
|
<form method="get" action="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/commits" style="margin-bottom: 1rem">
|
||||||
|
<label>Branch
|
||||||
|
<select name="ref">
|
||||||
|
{{range $d.Branches}}<option value="{{.Name}}" {{if eq .Name $d.Ref}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="submit">switch</button>
|
||||||
|
</form>
|
||||||
|
{{if $d.Commits}}
|
||||||
|
<table cellpadding="6"><tr><th align="left">Commit</th><th align="left">Author</th><th align="left">Date</th></tr>
|
||||||
|
{{range $d.Commits}}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/commit?ref={{urlquery $d.Ref}}&id={{urlquery .Hash}}"><code>{{shortHash .Hash}}</code> {{.Subject}}</a></td>
|
||||||
|
<td>{{.AuthorName}}</td>
|
||||||
|
<td>{{.Date}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{else}}<p>No commits.</p>{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "Commit"}}
|
||||||
|
{{$d := .Data}}
|
||||||
|
<h1>Commit {{shortHash $d.Commit.Hash}}</h1>
|
||||||
|
<p><a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/commits?ref={{urlquery $d.Ref}}">commit history</a> | <a href="/{{$d.Repo.Owner}}/{{$d.Repo.Name}}/tree?ref={{urlquery $d.Ref}}">tree</a></p>
|
||||||
|
<h2>{{$d.Commit.Subject}}</h2>
|
||||||
|
<p><code>{{$d.Commit.Hash}}</code></p>
|
||||||
|
<p>{{$d.Commit.AuthorName}} <{{$d.Commit.AuthorEmail}}> authored {{$d.Commit.Date}}</p>
|
||||||
|
{{if $d.Commit.Body}}<pre style="white-space: pre-wrap">{{$d.Commit.Body}}</pre>{{end}}
|
||||||
|
<h2>Diff</h2>
|
||||||
|
{{if $d.Commit.DiffTruncated}}<p>Diff truncated.</p>{{end}}
|
||||||
|
<pre style="white-space: pre-wrap">{{$d.Commit.Diff}}</pre>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "File"}}
|
{{define "File"}}
|
||||||
{{$d := .Data}}
|
{{$d := .Data}}
|
||||||
<h1>{{$d.Repo.Owner}}/{{$d.Repo.Name}}: {{$d.Path}}</h1>
|
<h1>{{$d.Repo.Owner}}/{{$d.Repo.Name}}: {{$d.Path}}</h1>
|
||||||
@@ -588,6 +659,18 @@ func (s *Server) webRepoRoute(w http.ResponseWriter, r *http.Request, parts []st
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.webRepoRaw(w, r, owner, name)
|
s.webRepoRaw(w, r, owner, name)
|
||||||
|
case "commits":
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
webError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.webCommitHistory(w, r, owner, name)
|
||||||
|
case "commit":
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
webError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.webCommitView(w, r, owner, name)
|
||||||
case "edit":
|
case "edit":
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
s.webRepoEdit(w, r, owner, name, "")
|
s.webRepoEdit(w, r, owner, name, "")
|
||||||
@@ -656,7 +739,15 @@ func (s *Server) webRepoTree(w http.ResponseWriter, r *http.Request, owner, name
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
branches, _ := gitRefs(s.repoPath(repo.Owner, repo.Name), "refs/heads")
|
branches, _ := gitRefs(s.repoPath(repo.Owner, repo.Name), "refs/heads")
|
||||||
data := webRepoData{Repo: repo, Ref: ref, Path: p, Entries: entries, CanWrite: authed && s.canWriteRepo(repo, user) && !repo.Archived, CanFork: authed && repo.Visibility == "public" && user.ID != repo.OwnerUserID, Branches: branches, CloneURL: s.publicURL + "/" + repo.Owner + "/" + repo.Name + ".git", ParentPath: parentRepoPath(p)}
|
var latest *webCommit
|
||||||
|
for _, b := range branches {
|
||||||
|
if b.Name == ref && b.Commit != "" {
|
||||||
|
latest = &webCommit{Hash: b.Commit, Subject: b.Message, Date: b.Date}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commitCount, _ := gitCommitCount(s.repoPath(repo.Owner, repo.Name), ref)
|
||||||
|
data := webRepoData{Repo: repo, Ref: ref, Path: p, Entries: entries, CanWrite: authed && s.canWriteRepo(repo, user) && !repo.Archived, CanFork: authed && repo.Visibility == "public" && user.ID != repo.OwnerUserID, Branches: branches, CloneURL: s.publicURL + "/" + repo.Owner + "/" + repo.Name + ".git", ParentPath: parentRepoPath(p), LatestCommit: latest, CommitCount: commitCount}
|
||||||
s.renderWeb(w, r, "Repository", data, "")
|
s.renderWeb(w, r, "Repository", data, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,6 +838,40 @@ func (s *Server) webBranchDeletePost(w http.ResponseWriter, r *http.Request, own
|
|||||||
http.Redirect(w, r, "/"+repo.Owner+"/"+repo.Name+"/branches", http.StatusSeeOther)
|
http.Redirect(w, r, "/"+repo.Owner+"/"+repo.Name+"/branches", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) webCommitHistory(w http.ResponseWriter, r *http.Request, owner, name string) {
|
||||||
|
repo, _, _, ok := s.webRepoContext(w, r, owner, name)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ref := webRef(r, repo)
|
||||||
|
branches, _ := gitRefs(s.repoPath(repo.Owner, repo.Name), "refs/heads")
|
||||||
|
commits, err := gitCommitHistory(s.repoPath(repo.Owner, repo.Name), ref, 100)
|
||||||
|
if err != nil {
|
||||||
|
webError(w, r, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.renderWeb(w, r, "Commit History", webCommitHistoryData{Repo: repo, Ref: ref, Branches: branches, Commits: commits}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) webCommitView(w http.ResponseWriter, r *http.Request, owner, name string) {
|
||||||
|
repo, _, _, ok := s.webRepoContext(w, r, owner, name)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ref := webRef(r, repo)
|
||||||
|
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||||
|
if id == "" {
|
||||||
|
webError(w, r, http.StatusBadRequest, "commit id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commit, err := gitCommitDetail(s.repoPath(repo.Owner, repo.Name), ref, id)
|
||||||
|
if err != nil {
|
||||||
|
webError(w, r, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.renderWeb(w, r, "Commit", webCommitData{Repo: repo, Ref: ref, Commit: commit}, "")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) webRepoBlob(w http.ResponseWriter, r *http.Request, owner, name string) {
|
func (s *Server) webRepoBlob(w http.ResponseWriter, r *http.Request, owner, name string) {
|
||||||
repo, user, authed, ok := s.webRepoContext(w, r, owner, name)
|
repo, user, authed, ok := s.webRepoContext(w, r, owner, name)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -1265,6 +1390,21 @@ func webRef(r *http.Request, repo Repository) string {
|
|||||||
return ref
|
return ref
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shortHash(hash string) string {
|
||||||
|
if len(hash) > 10 {
|
||||||
|
return hash[:10]
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(s string, n int) string {
|
||||||
|
if n <= 0 || len([]rune(s)) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
r := []rune(s)
|
||||||
|
return string(r[:n-1]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
func cleanRepoFilePath(p string, allowEmpty bool) (string, error) {
|
func cleanRepoFilePath(p string, allowEmpty bool) (string, error) {
|
||||||
p = strings.TrimSpace(strings.ReplaceAll(p, "\\", "/"))
|
p = strings.TrimSpace(strings.ReplaceAll(p, "\\", "/"))
|
||||||
if p == "" {
|
if p == "" {
|
||||||
@@ -1343,6 +1483,130 @@ func gitListTree(repoPath, ref, p string) ([]webTreeEntry, error) {
|
|||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gitCommitCount(repoPath, ref string) (int, error) {
|
||||||
|
if !branchRE.MatchString(ref) {
|
||||||
|
return 0, errors.New("invalid branch")
|
||||||
|
}
|
||||||
|
if !gitBranchExists(repoPath, ref) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
out, err := exec.Command("git", "--git-dir", repoPath, "rev-list", "--count", "refs/heads/"+ref).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("git rev-list failed: %s", strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
count, _ := strconv.Atoi(strings.TrimSpace(string(out)))
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitCommitHistory(repoPath, ref string, limit int) ([]webCommit, error) {
|
||||||
|
if !branchRE.MatchString(ref) {
|
||||||
|
return nil, errors.New("invalid branch")
|
||||||
|
}
|
||||||
|
if !gitBranchExists(repoPath, ref) {
|
||||||
|
return nil, errors.New("branch not found")
|
||||||
|
}
|
||||||
|
if limit <= 0 || limit > 500 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
format := "%H%x00%an%x00%ae%x00%ct%x00%s%x00%b%x1e"
|
||||||
|
out, err := exec.Command("git", "--git-dir", repoPath, "log", "-n", strconv.Itoa(limit), "--format="+format, "refs/heads/"+ref).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("git log failed: %s", strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
return parseGitCommits(string(out)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitCommitDetail(repoPath, ref, id string) (webCommit, error) {
|
||||||
|
full, err := resolveCommitInBranch(repoPath, ref, id)
|
||||||
|
if err != nil {
|
||||||
|
return webCommit{}, err
|
||||||
|
}
|
||||||
|
format := "%H%x00%an%x00%ae%x00%ct%x00%s%x00%b%x1e"
|
||||||
|
out, err := exec.Command("git", "--git-dir", repoPath, "show", "-s", "--format="+format, full).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return webCommit{}, fmt.Errorf("git show failed: %s", strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
commits := parseGitCommits(string(out))
|
||||||
|
if len(commits) == 0 {
|
||||||
|
return webCommit{}, errors.New("commit not found")
|
||||||
|
}
|
||||||
|
commit := commits[0]
|
||||||
|
diff, truncated, err := gitCommitDiff(repoPath, full)
|
||||||
|
if err != nil {
|
||||||
|
return webCommit{}, err
|
||||||
|
}
|
||||||
|
commit.Diff = diff
|
||||||
|
commit.DiffTruncated = truncated
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGitCommits(out string) []webCommit {
|
||||||
|
var commits []webCommit
|
||||||
|
for _, rec := range strings.Split(out, "\x1e") {
|
||||||
|
rec = strings.Trim(rec, "\n")
|
||||||
|
if rec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(rec, "\x00", 6)
|
||||||
|
if len(parts) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
unix, _ := strconv.ParseInt(strings.TrimSpace(parts[3]), 10, 64)
|
||||||
|
commits = append(commits, webCommit{Hash: parts[0], AuthorName: parts[1], AuthorEmail: parts[2], Date: time.Unix(unix, 0).UTC(), Subject: parts[4], Body: strings.TrimSpace(parts[5])})
|
||||||
|
}
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCommitInBranch(repoPath, ref, id string) (string, error) {
|
||||||
|
if !branchRE.MatchString(ref) {
|
||||||
|
return "", errors.New("invalid branch")
|
||||||
|
}
|
||||||
|
if !gitBranchExists(repoPath, ref) {
|
||||||
|
return "", errors.New("branch not found")
|
||||||
|
}
|
||||||
|
if !validCommitID(id) {
|
||||||
|
return "", errors.New("invalid commit id")
|
||||||
|
}
|
||||||
|
out, err := exec.Command("git", "--git-dir", repoPath, "rev-parse", "--verify", id+"^{commit}").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("commit not found")
|
||||||
|
}
|
||||||
|
full := strings.TrimSpace(string(out))
|
||||||
|
if err := exec.Command("git", "--git-dir", repoPath, "merge-base", "--is-ancestor", full, "refs/heads/"+ref).Run(); err != nil {
|
||||||
|
return "", errors.New("commit is not on branch")
|
||||||
|
}
|
||||||
|
return full, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validCommitID(id string) bool {
|
||||||
|
if len(id) < 4 || len(id) > 64 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range id {
|
||||||
|
if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitCommitDiff(repoPath, full string) (string, bool, error) {
|
||||||
|
out, err := exec.Command("git", "--git-dir", repoPath, "show", "--format=", "--patch", "--find-renames", "--no-ext-diff", "--unified=3", "--no-color", full).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("git show diff failed: %s", strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
truncated := false
|
||||||
|
if len(out) > maxWebDiffBytes {
|
||||||
|
out = out[:maxWebDiffBytes]
|
||||||
|
for !utf8.Valid(out) && len(out) > 0 {
|
||||||
|
out = out[:len(out)-1]
|
||||||
|
}
|
||||||
|
truncated = true
|
||||||
|
}
|
||||||
|
return string(out), truncated, nil
|
||||||
|
}
|
||||||
|
|
||||||
func gitReadBlob(repoPath, ref, p string) (string, error) {
|
func gitReadBlob(repoPath, ref, p string) (string, error) {
|
||||||
out, err := gitReadBlobBytes(repoPath, ref, p)
|
out, err := gitReadBlobBytes(repoPath, ref, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -122,10 +122,37 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
|
|||||||
expectLoadRepo(mock, "alice", "demo", repo)
|
expectLoadRepo(mock, "alice", "demo", repo)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo", nil))
|
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo", nil))
|
||||||
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "README.md") || !strings.Contains(rr.Body.String(), "Clone:") {
|
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "README.md") || !strings.Contains(rr.Body.String(), "Clone:") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "2 commits") {
|
||||||
t.Fatalf("repo page status=%d body=%s", rr.Code, rr.Body.String())
|
t.Fatalf("repo page status=%d body=%s", rr.Code, rr.Body.String())
|
||||||
}
|
}
|
||||||
csrf := csrfFromResponse(t, rr)
|
csrf := csrfFromResponse(t, rr)
|
||||||
|
refs, err := gitRefs(bare, "refs/heads")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var mainHash string
|
||||||
|
for _, ref := range refs {
|
||||||
|
if ref.Name == "main" {
|
||||||
|
mainHash = ref.Commit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mainHash == "" {
|
||||||
|
t.Fatal("missing main branch hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectLoadRepo(mock, "alice", "demo", repo)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commits?ref=main", nil))
|
||||||
|
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commits for alice/demo") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "initial") || !strings.Contains(rr.Body.String(), "/alice/demo/commit?ref=main") {
|
||||||
|
t.Fatalf("commit history status=%d body=%s", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
expectLoadRepo(mock, "alice", "demo", repo)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commit?ref=main&id="+mainHash, nil))
|
||||||
|
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commit ") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "diff --git") || !strings.Contains(rr.Body.String(), "docs.md") {
|
||||||
|
t.Fatalf("commit view status=%d body=%s", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
expectLoadRepo(mock, "alice", "demo", repo)
|
expectLoadRepo(mock, "alice", "demo", repo)
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ go test -cover ./...
|
|||||||
Current default coverage snapshot after Phase 4 implementation:
|
Current default coverage snapshot after Phase 4 implementation:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/app 59.8%
|
internal/app 60.2%
|
||||||
internal/backup 42.3%
|
internal/backup 42.3%
|
||||||
internal/config 79.5%
|
internal/config 79.5%
|
||||||
internal/dbutil 8.6%
|
internal/dbutil 8.6%
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ Goal: extend the minimal web UI with branch workflows, pull request workflows, a
|
|||||||
- [x] Create an empty branch with an empty root commit
|
- [x] Create an empty branch with an empty root commit
|
||||||
- [x] Delete non-default branches from the branches page
|
- [x] Delete non-default branches from the branches page
|
||||||
- [x] No branch rename support in API or web UI
|
- [x] No branch rename support in API or web UI
|
||||||
|
- [x] Show latest selected-branch commit and selected-branch commit count on repository pages
|
||||||
|
- [x] Commit history page for selected branches
|
||||||
|
- [x] Single commit page with limited diff rendering
|
||||||
- [x] List pull requests in the web UI
|
- [x] List pull requests in the web UI
|
||||||
- [x] Create pull requests in the web UI
|
- [x] Create pull requests in the web UI
|
||||||
- [x] View pull request details and comments
|
- [x] View pull request details and comments
|
||||||
|
|||||||
Reference in New Issue
Block a user