feat: add git-bug integration to web UI

Amolith and Crush created

Implement web interface for displaying git-bug issues including
bug listing page, individual bug details, and navigation integration.
Add styling and handlers for bug display functionality.

Implements: bug-7d32125
Co-Authored-By: Crush <crush@charm.land>

Change summary

pkg/web/static/overrides.css |  42 +++
pkg/web/templates/base.html  |   3 
pkg/web/templates/bug.html   |  88 +++++++
pkg/web/templates/bugs.html  |  64 +++++
pkg/web/webui.go             |   7 
pkg/web/webui_blob.go        |   1 
pkg/web/webui_branches.go    |   1 
pkg/web/webui_bugs.go        | 448 ++++++++++++++++++++++++++++++++++++++
pkg/web/webui_commit.go      |   1 
pkg/web/webui_commits.go     |   1 
pkg/web/webui_overview.go    |   1 
pkg/web/webui_tags.go        |   1 
pkg/web/webui_tree.go        |   1 
13 files changed, 658 insertions(+), 1 deletion(-)

Detailed changes

pkg/web/static/overrides.css 🔗

@@ -103,6 +103,46 @@ section[aria-labelledby="diff-heading"] article h4:focus-within .secondary {
   --pico-font-family-monospace: "0xProto","JetBrains Mono",ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);
 }
 
+.badge {
+  display: inline-block;
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+  font-weight: 600;
+  line-height: 1;
+  border-radius: 0.25rem;
+  text-transform: lowercase;
+}
+
+.badge-open {
+  background-color: #22c55e;
+  color: #ffffff;
+}
+
+.badge-closed {
+  background-color: #ef4444;
+  color: #ffffff;
+}
+
+.bug-message {
+  margin: 1rem 0;
+}
+
+.muted {
+  color: var(--pico-muted-color);
+}
+
+.visually-hidden {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border: 0;
+}
+
 article > header > h1,
 article > header > h2,
 article > header > h3,
@@ -115,4 +155,4 @@ article > header > p {
 
 article > p:only-child {
 	margin-bottom: 0;
-}	
+}

pkg/web/templates/base.html 🔗

@@ -49,6 +49,9 @@
       </ul>
       <ul>
         <li><a href="/{{.Repo.Name}}"{{if eq .ActiveTab "overview"}} aria-current="page"{{end}}>README</a></li>
+        {{if .HasGitBug}}
+        <li><a href="/{{.Repo.Name}}/bugs"{{if eq .ActiveTab "bugs"}} aria-current="page"{{end}}>Bugs</a></li>
+        {{end}}
         <li><a href="/{{.Repo.Name}}/tree/{{.DefaultBranch}}"{{if eq .ActiveTab "tree"}} aria-current="page"{{end}}>Files</a></li>
         <li><a href="/{{.Repo.Name}}/commits/{{.DefaultBranch}}"{{if eq .ActiveTab "commits"}} aria-current="page"{{end}}>Commits</a></li>
         <li><a href="/{{.Repo.Name}}/branches"{{if eq .ActiveTab "branches"}} aria-current="page"{{end}}>Branches</a></li>

pkg/web/templates/bug.html 🔗

@@ -0,0 +1,88 @@
+{{define "content"}}
+<nav aria-label="breadcrumb">
+  <ul>
+    <li><a href="/{{.Repo.Name}}">{{.Repo.Name}}</a></li>
+    <li><a href="/{{.Repo.Name}}/bugs">Bugs</a></li>
+    <li aria-current="page">{{.ID}}</li>
+  </ul>
+</nav>
+
+<header>
+  <h2 id="bug-heading">
+    [{{.ID}}] {{.Title}}
+    <span class="badge badge-{{.Status}}">{{.Status}}</span>
+  </h2>
+  <p>
+    <strong>{{.Author}}</strong> opened this bug on
+    <time datetime="{{.CreatedAt | rfc3339}}">{{.CreatedAt | formatDate}}</time>
+    {{if .Edited}}<em>(edited)</em>{{end}}
+  </p>
+  {{if .Labels}}
+  <div aria-label="Labels">
+    {{range .Labels}}
+    <span class="label" style="background-color: {{.Color}}; color: white; padding: 0.25rem 0.5rem; border-radius: 0.25rem; display: inline-block; margin: 0.25rem;">{{.Name}}</span>
+    {{end}}
+  </div>
+  {{end}}
+</header>
+
+<section aria-labelledby="timeline-heading">
+  <h3 id="timeline-heading" class="visually-hidden">Timeline</h3>
+  
+  {{range .Timeline}}
+  <article id="{{.ID}}" aria-labelledby="timeline-item-{{.ID}}">
+    {{if eq .Type "create"}}
+    <h4 id="timeline-item-{{.ID}}" class="visually-hidden">Initial description</h4>
+    {{if .Message}}
+    <div class="bug-message">
+      {{.Message}}
+    </div>
+    {{else}}
+    <p><em class="muted">No description provided.</em></p>
+    {{end}}
+    
+    {{else if eq .Type "comment"}}
+    <h4 id="timeline-item-{{.ID}}">
+      <strong>{{.Author}}</strong> commented on
+      <time datetime="{{.Timestamp | rfc3339}}">{{.Timestamp | formatDate}}</time>
+      {{if .Edited}}<em>(edited)</em>{{end}}
+    </h4>
+    {{if .Message}}
+    <div class="bug-message">
+      {{.Message}}
+    </div>
+    {{else}}
+    <p><em class="muted">No comment provided.</em></p>
+    {{end}}
+    
+    {{else if eq .Type "title"}}
+    <p id="timeline-item-{{.ID}}">
+      <strong>{{.Author}}</strong> changed the title to <strong>{{.Title}}</strong> on
+      <time datetime="{{.Timestamp | rfc3339}}">{{.Timestamp | formatDate}}</time>
+    </p>
+    
+    {{else if eq .Type "status"}}
+    <p id="timeline-item-{{.ID}}">
+      <strong>{{.Author}}</strong> {{.Status}} the bug on
+      <time datetime="{{.Timestamp | rfc3339}}">{{.Timestamp | formatDate}}</time>
+    </p>
+    
+    {{else if eq .Type "labels"}}
+    <p id="timeline-item-{{.ID}}">
+      <strong>{{.Author}}</strong>
+      {{if .AddedLabels}}
+      added label{{if gt (len .AddedLabels) 1}}s{{end}}
+      {{range $i, $label := .AddedLabels}}{{if $i}}, {{end}}<code>{{$label}}</code>{{end}}
+      {{end}}
+      {{if and .AddedLabels .RemovedLabels}}and{{end}}
+      {{if .RemovedLabels}}
+      removed label{{if gt (len .RemovedLabels) 1}}s{{end}}
+      {{range $i, $label := .RemovedLabels}}{{if $i}}, {{end}}<code>{{$label}}</code>{{end}}
+      {{end}}
+      on <time datetime="{{.Timestamp | rfc3339}}">{{.Timestamp | formatDate}}</time>
+    </p>
+    {{end}}
+  </article>
+  {{end}}
+</section>
+{{end}}

pkg/web/templates/bugs.html 🔗

@@ -0,0 +1,64 @@
+{{define "content"}}
+<section aria-labelledby="bugs-heading">
+  <h2 id="bugs-heading">Bugs</h2>
+
+  <nav aria-label="Bug status filter">
+    <ul>
+      <li><a href="/{{.Repo.Name}}/bugs?status=all"{{if eq .Status "all"}} aria-current="page"{{end}}>All</a></li>
+      <li><a href="/{{.Repo.Name}}/bugs?status=open"{{if eq .Status "open"}} aria-current="page"{{end}}>Open</a></li>
+      <li><a href="/{{.Repo.Name}}/bugs?status=closed"{{if eq .Status "closed"}} aria-current="page"{{end}}>Closed</a></li>
+    </ul>
+  </nav>
+
+  {{if .Bugs}}
+  {{range .Bugs}}
+  <article aria-labelledby="bug-{{.ID}}">
+    <h3 id="bug-{{.ID}}">
+      <code><a href="/{{$.Repo.Name}}/bug/{{.FullID}}">{{.ID}}</a></code>
+      {{.Title}}
+      <span class="badge badge-{{.Status}}">{{.Status}}</span>
+    </h3>
+    <p>
+      by <strong>{{.Author}}</strong> on
+      <time datetime="{{.CreatedAt | rfc3339}}">{{.CreatedAt | formatDate}}</time>
+      • Last activity:
+      <time datetime="{{.LastActivity | rfc3339}}">{{.LastActivity | formatDate}}</time>
+      {{if gt .CommentCount 0}}
+      • {{.CommentCount}} comment{{if ne .CommentCount 1}}s{{end}}
+      {{end}}
+    </p>
+  </article>
+  {{end}}
+
+  {{if or .HasPrevPage .HasNextPage}}
+  <nav aria-label="Pagination">
+    <p>Page {{.Page}} of {{.TotalPages}}</p>
+    <ul>
+      {{if .HasPrevPage}}
+      <li>
+        <a href="/{{.Repo.Name}}/bugs?status={{.Status}}&page={{dec .Page}}" rel="prev"
+          >Previous page</a
+        >
+      </li>
+      {{end}}
+      {{if .HasNextPage}}
+      <li>
+        <a href="/{{.Repo.Name}}/bugs?status={{.Status}}&page={{inc .Page}}" rel="next"
+          >Next page</a
+        >
+      </li>
+      {{end}}
+    </ul>
+  </nav>
+  {{end}}
+  {{else}}
+  {{if eq .Status "all"}}
+  <p>git-bug is initialised, but this repo has no bugs.</p>
+  {{else if eq .Status "open"}}
+  <p>No open bugs.</p>
+  {{else if eq .Status "closed"}}
+  <p>No closed bugs.</p>
+  {{end}}
+  {{end}}
+</section>
+{{end}}

pkg/web/webui.go 🔗

@@ -51,6 +51,7 @@ type RepoBaseData struct {
 	BaseData
 	Repo          proto.Repository
 	DefaultBranch string
+	HasGitBug     bool
 }
 
 // PaginationData contains common fields for paginated views.
@@ -365,6 +366,12 @@ func WebUIController(ctx context.Context, r *mux.Router) {
 	r.Handle(basePrefix+"/tags", withRepoVars(withWebUIAccess(http.HandlerFunc(repoTags)))).
 		Methods(http.MethodGet)
 
+	// Bugs routes
+	r.Handle(basePrefix+"/bugs", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBugs)))).
+		Methods(http.MethodGet)
+	r.Handle(basePrefix+"/bug/{hash:.+}", withRepoVars(withWebUIAccess(http.HandlerFunc(repoBug)))).
+		Methods(http.MethodGet)
+
 	// Repository overview (catch-all, must be last)
 	r.Handle(basePrefix, withRepoVars(withWebUIAccess(http.HandlerFunc(repoOverview)))).
 		Methods(http.MethodGet)

pkg/web/webui_blob.go 🔗

@@ -117,6 +117,7 @@ func repoBlob(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		Ref:          ref,
 		Path:         path,

pkg/web/webui_branches.go 🔗

@@ -105,6 +105,7 @@ func repoBranches(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		PaginationData: PaginationData{
 			Page:        page,

pkg/web/webui_bugs.go 🔗

@@ -0,0 +1,448 @@
+package web
+
+import (
+	"context"
+	"fmt"
+	"html/template"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sort"
+	"strconv"
+	"time"
+
+	"github.com/charmbracelet/log/v2"
+	"github.com/charmbracelet/soft-serve/pkg/config"
+	"github.com/charmbracelet/soft-serve/pkg/proto"
+	"github.com/git-bug/git-bug/cache"
+	"github.com/git-bug/git-bug/entities/bug"
+	"github.com/git-bug/git-bug/entities/common"
+	"github.com/git-bug/git-bug/repository"
+	"github.com/gorilla/mux"
+)
+
+const defaultBugsPerPage = 20
+
+type BugsData struct {
+	Repo          proto.Repository
+	DefaultBranch string
+	ActiveTab     string
+	ServerName    string
+	HasGitBug     bool
+
+	Bugs        []BugListItem
+	Status      string
+	Page        int
+	TotalPages  int
+	HasPrevPage bool
+	HasNextPage bool
+}
+
+type BugListItem struct {
+	ID           string
+	FullID       string
+	Title        string
+	Author       string
+	Status       string
+	CreatedAt    time.Time
+	LastActivity time.Time
+	CommentCount int
+}
+
+type BugData struct {
+	Repo          proto.Repository
+	DefaultBranch string
+	ActiveTab     string
+	ServerName    string
+	HasGitBug     bool
+
+	ID        string
+	Title     string
+	Status    string
+	Author    string
+	CreatedAt time.Time
+	Edited    bool
+	Timeline  []TimelineItem
+	Labels    []Label
+}
+
+type TimelineItem struct {
+	Type      string
+	ID        string
+	Author    string
+	Timestamp time.Time
+	Edited    bool
+
+	Message       template.HTML
+	Title         string
+	Status        string
+	AddedLabels   []string
+	RemovedLabels []string
+}
+
+type Label struct {
+	Name  string
+	Color string
+}
+
+func hasGitBug(ctx context.Context, repo proto.Repository) bool {
+	cfg := config.FromContext(ctx)
+	repoPath := filepath.Join(cfg.DataPath, "repos", repo.Name()+".git")
+	bugsPath := filepath.Join(repoPath, "refs", "bugs")
+
+	info, err := os.Stat(bugsPath)
+	if err != nil {
+		return false
+	}
+	return info.IsDir()
+}
+
+func openBugCache(ctx context.Context, repo proto.Repository) (*cache.RepoCache, error) {
+	cfg := config.FromContext(ctx)
+	repoPath := filepath.Join(cfg.DataPath, "repos", repo.Name()+".git")
+
+	goGitRepo, err := repository.OpenGoGitRepo(repoPath, "git-bug", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	rc, err := cache.NewRepoCacheNoEvents(goGitRepo)
+	if err != nil {
+		goGitRepo.Close()
+		return nil, err
+	}
+
+	return rc, nil
+}
+
+func getBugsList(rc *cache.RepoCache, status string) ([]BugListItem, error) {
+	allBugs := rc.Bugs().AllIds()
+	bugs := make([]BugListItem, 0)
+
+	for _, id := range allBugs {
+		bugCache, err := rc.Bugs().Resolve(id)
+		if err != nil {
+			continue
+		}
+
+		snap := bugCache.Snapshot()
+
+		if status != "all" && snap.Status.String() != status {
+			continue
+		}
+
+		bugs = append(bugs, BugListItem{
+			ID:           snap.Id().Human(),
+			FullID:       snap.Id().String(),
+			Title:        snap.Title,
+			Author:       snap.Author.DisplayName(),
+			Status:       snap.Status.String(),
+			CreatedAt:    snap.CreateTime,
+			LastActivity: getLastActivity(snap),
+			CommentCount: countComments(snap),
+		})
+	}
+
+	sort.Slice(bugs, func(i, j int) bool {
+		return bugs[i].LastActivity.After(bugs[j].LastActivity)
+	})
+
+	return bugs, nil
+}
+
+func getLastActivity(snap *bug.Snapshot) time.Time {
+	var lastTime time.Time
+
+	for _, item := range snap.Timeline {
+		var itemTime time.Time
+
+		switch op := item.(type) {
+		case *bug.CreateTimelineItem:
+			itemTime = op.CreatedAt.Time()
+		case *bug.AddCommentTimelineItem:
+			itemTime = op.CreatedAt.Time()
+		case *bug.SetTitleTimelineItem:
+			itemTime = op.UnixTime.Time()
+		case *bug.SetStatusTimelineItem:
+			itemTime = op.UnixTime.Time()
+		case *bug.LabelChangeTimelineItem:
+			itemTime = op.UnixTime.Time()
+		}
+
+		if itemTime.After(lastTime) {
+			lastTime = itemTime
+		}
+	}
+
+	return lastTime
+}
+
+func countComments(snap *bug.Snapshot) int {
+	count := 0
+	for _, item := range snap.Timeline {
+		if _, ok := item.(*bug.AddCommentTimelineItem); ok {
+			count++
+		}
+	}
+	return count
+}
+
+func buildTimelineItems(snap *bug.Snapshot) []TimelineItem {
+	items := make([]TimelineItem, 0, len(snap.Timeline))
+
+	for _, item := range snap.Timeline {
+		switch op := item.(type) {
+		case *bug.CreateTimelineItem:
+			messageHTML := template.HTML("")
+			if !op.MessageIsEmpty() {
+				rendered, err := renderMarkdown([]byte(op.Message))
+				if err != nil {
+					messageHTML = template.HTML(template.HTMLEscapeString(op.Message))
+				} else {
+					messageHTML = rendered
+				}
+			}
+
+			items = append(items, TimelineItem{
+				Type:      "create",
+				ID:        op.CombinedId().String(),
+				Author:    op.Author.DisplayName(),
+				Timestamp: op.CreatedAt.Time(),
+				Edited:    op.Edited(),
+				Message:   messageHTML,
+			})
+
+		case *bug.AddCommentTimelineItem:
+			messageHTML := template.HTML("")
+			if !op.MessageIsEmpty() {
+				rendered, err := renderMarkdown([]byte(op.Message))
+				if err != nil {
+					messageHTML = template.HTML(template.HTMLEscapeString(op.Message))
+				} else {
+					messageHTML = rendered
+				}
+			}
+
+			items = append(items, TimelineItem{
+				Type:      "comment",
+				ID:        op.CombinedId().String(),
+				Author:    op.Author.DisplayName(),
+				Timestamp: op.CreatedAt.Time(),
+				Edited:    op.Edited(),
+				Message:   messageHTML,
+			})
+
+		case *bug.SetTitleTimelineItem:
+			items = append(items, TimelineItem{
+				Type:      "title",
+				ID:        op.CombinedId().String(),
+				Author:    op.Author.DisplayName(),
+				Timestamp: op.UnixTime.Time(),
+				Title:     op.Title,
+			})
+
+		case *bug.SetStatusTimelineItem:
+			items = append(items, TimelineItem{
+				Type:      "status",
+				ID:        op.CombinedId().String(),
+				Author:    op.Author.DisplayName(),
+				Timestamp: op.UnixTime.Time(),
+				Status:    op.Status.Action(),
+			})
+
+		case *bug.LabelChangeTimelineItem:
+			added := make([]string, len(op.Added))
+			for i, label := range op.Added {
+				added[i] = label.String()
+			}
+
+			removed := make([]string, len(op.Removed))
+			for i, label := range op.Removed {
+				removed[i] = label.String()
+			}
+
+			items = append(items, TimelineItem{
+				Type:          "labels",
+				ID:            op.CombinedId().String(),
+				Author:        op.Author.DisplayName(),
+				Timestamp:     op.UnixTime.Time(),
+				AddedLabels:   added,
+				RemovedLabels: removed,
+			})
+		}
+	}
+
+	return items
+}
+
+func findBugByHash(rc *cache.RepoCache, hash string) (*cache.BugCache, error) {
+	allBugs := rc.Bugs().AllIds()
+	for _, id := range allBugs {
+		if id.String() == hash || id.Human() == hash {
+			return rc.Bugs().Resolve(id)
+		}
+	}
+	return nil, fmt.Errorf("bug not found")
+}
+
+func labelToWebLabel(label common.Label) Label {
+	rgba := label.Color().RGBA()
+	return Label{
+		Name:  label.String(),
+		Color: fmt.Sprintf("#%02x%02x%02x", rgba.R, rgba.G, rgba.B),
+	}
+}
+
+func repoBugs(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	logger := log.FromContext(ctx)
+	cfg := config.FromContext(ctx)
+	repo := proto.RepositoryFromContext(ctx)
+
+	if !hasGitBug(ctx, repo) {
+		renderNotFound(w, r)
+		return
+	}
+
+	rc, err := openBugCache(ctx, repo)
+	if err != nil {
+		logger.Debug("failed to open bug cache", "repo", repo.Name(), "err", err)
+		renderNotFound(w, r)
+		return
+	}
+	defer rc.Close()
+
+	status := r.URL.Query().Get("status")
+	if status == "" {
+		status = "all"
+	}
+	if status != "all" && status != "open" && status != "closed" {
+		status = "all"
+	}
+
+	page := 1
+	if pageStr := r.URL.Query().Get("page"); pageStr != "" {
+		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
+			page = p
+		}
+	}
+
+	bugs, err := getBugsList(rc, status)
+	if err != nil {
+		logger.Debug("failed to get bugs list", "repo", repo.Name(), "err", err)
+		renderInternalServerError(w, r)
+		return
+	}
+
+	totalBugs := len(bugs)
+	totalPages := (totalBugs + defaultBugsPerPage - 1) / defaultBugsPerPage
+	if totalPages < 1 {
+		totalPages = 1
+	}
+	if page > totalPages && totalPages > 0 {
+		page = totalPages
+	}
+	if page < 1 {
+		page = 1
+	}
+
+	start := (page - 1) * defaultBugsPerPage
+	end := start + defaultBugsPerPage
+	if end > totalBugs {
+		end = totalBugs
+	}
+
+	pagedBugs := bugs[start:end]
+
+	gr, err := openRepository(repo)
+	if err != nil {
+		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
+		renderInternalServerError(w, r)
+		return
+	}
+	defaultBranch := getDefaultBranch(gr)
+
+	data := BugsData{
+		Repo:          repo,
+		DefaultBranch: defaultBranch,
+		ActiveTab:     "bugs",
+		ServerName:    cfg.Name,
+		HasGitBug:     true,
+		Bugs:          pagedBugs,
+		Status:        status,
+		Page:          page,
+		TotalPages:    totalPages,
+		HasPrevPage:   page > 1,
+		HasNextPage:   page < totalPages,
+	}
+
+	renderHTML(w, "bugs.html", data)
+}
+
+func repoBug(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	logger := log.FromContext(ctx)
+	cfg := config.FromContext(ctx)
+	repo := proto.RepositoryFromContext(ctx)
+	vars := mux.Vars(r)
+	hash := vars["hash"]
+
+	if !hasGitBug(ctx, repo) {
+		renderNotFound(w, r)
+		return
+	}
+
+	rc, err := openBugCache(ctx, repo)
+	if err != nil {
+		logger.Debug("failed to open bug cache", "repo", repo.Name(), "err", err)
+		renderNotFound(w, r)
+		return
+	}
+	defer rc.Close()
+
+	bugCache, err := findBugByHash(rc, hash)
+	if err != nil {
+		logger.Debug("failed to find bug", "repo", repo.Name(), "hash", hash, "err", err)
+		renderNotFound(w, r)
+		return
+	}
+
+	snap := bugCache.Snapshot()
+	timeline := buildTimelineItems(snap)
+
+	labels := make([]Label, len(snap.Labels))
+	for i, label := range snap.Labels {
+		labels[i] = labelToWebLabel(label)
+	}
+
+	var edited bool
+	if len(timeline) > 0 && timeline[0].Type == "create" {
+		edited = timeline[0].Edited
+	}
+
+	gr, err := openRepository(repo)
+	if err != nil {
+		logger.Debug("failed to open repository", "repo", repo.Name(), "err", err)
+		renderInternalServerError(w, r)
+		return
+	}
+	defaultBranch := getDefaultBranch(gr)
+
+	data := BugData{
+		Repo:          repo,
+		DefaultBranch: defaultBranch,
+		ActiveTab:     "bugs",
+		ServerName:    cfg.Name,
+		HasGitBug:     true,
+		ID:            snap.Id().Human(),
+		Title:         snap.Title,
+		Status:        snap.Status.String(),
+		Author:        snap.Author.DisplayName(),
+		CreatedAt:     snap.CreateTime,
+		Edited:        edited,
+		Timeline:      timeline,
+		Labels:        labels,
+	}
+
+	renderHTML(w, "bug.html", data)
+}

pkg/web/webui_commit.go 🔗

@@ -90,6 +90,7 @@ func repoCommit(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		Commit:    commit,
 		Diff:      diff,

pkg/web/webui_commits.go 🔗

@@ -99,6 +99,7 @@ func repoCommits(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		PaginationData: PaginationData{
 			Page:        page,

pkg/web/webui_overview.go 🔗

@@ -65,6 +65,7 @@ func repoOverview(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		IsEmpty:    isEmpty,
 		SSHURL:     sshURL,

pkg/web/webui_tags.go 🔗

@@ -125,6 +125,7 @@ func repoTags(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		PaginationData: PaginationData{
 			Page:        page,

pkg/web/webui_tree.go 🔗

@@ -113,6 +113,7 @@ func repoTree(w http.ResponseWriter, r *http.Request) {
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,
+			HasGitBug:     hasGitBug(ctx, repo),
 		},
 		Ref:          ref,
 		Path:         path,