+
{{ code .BlobView.Contents .Path | escapeHtml }}
{{ end }}
{{ template "fragments/multiline-select" }}
+
{{ end }}
--
2.51.2
From 92ab4c2ea4a9f36392179c5539140aa47c84a9a5 Mon Sep 17 00:00:00 2001
From: Patrick Dewey
Date: Sat, 7 Feb 2026 18:01:06 -0500
Subject: [PATCH 042/122] appview/issues: fix search not updating count of
open/closed issues
searching for issues did not previously update open/closed issue counts
https://tangled.org/tangled.org/core/issues/400.
Signed-off-by: pdewey.com
---
appview/issues/issues.go | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/appview/issues/issues.go b/appview/issues/issues.go
index 9d35bb71..555f73d3 100644
--- a/appview/issues/issues.go
+++ b/appview/issues/issues.go
@@ -822,6 +822,8 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) {
keyword := params.Get("q")
+ repoInfo := rp.repoResolver.GetRepoInfo(r, user)
+
var issues []models.Issue
searchOpts := models.IssueSearchOptions{
Keyword: keyword,
@@ -838,6 +840,21 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) {
l.Debug("searched issues with indexer", "count", len(res.Hits))
totalIssues = int(res.Total)
+ // count matching issues in the opposite state to display correct counts
+ countRes, err := rp.indexer.Search(r.Context(), models.IssueSearchOptions{
+ Keyword: keyword, RepoAt: f.RepoAt().String(), IsOpen: !isOpen,
+ Page: pagination.Page{Limit: 1},
+ })
+ if err == nil {
+ if isOpen {
+ repoInfo.Stats.IssueCount.Open = int(res.Total)
+ repoInfo.Stats.IssueCount.Closed = int(countRes.Total)
+ } else {
+ repoInfo.Stats.IssueCount.Closed = int(res.Total)
+ repoInfo.Stats.IssueCount.Open = int(countRes.Total)
+ }
+ }
+
issues, err = db.GetIssues(
rp.db,
orm.FilterIn("id", res.Hits),
@@ -884,7 +901,7 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) {
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: rp.oauth.GetMultiAccountUser(r),
- RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+ RepoInfo: repoInfo,
Issues: issues,
IssueCount: totalIssues,
LabelDefs: defs,
--
2.51.2
From c23e99cf05759c343d70cdc018fb8e3b82e1d68e Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Wed, 28 Jan 2026 20:30:52 +0200
Subject: [PATCH 043/122] cmd/populatepipelines: script to generate dummy
pipeline runs
Signed-off-by: Anirudh Oppiliappan
---
cmd/populatepipelines/populate_pipelines.go | 242 ++++++++++++++++++++
1 file changed, 242 insertions(+)
create mode 100644 cmd/populatepipelines/populate_pipelines.go
diff --git a/cmd/populatepipelines/populate_pipelines.go b/cmd/populatepipelines/populate_pipelines.go
new file mode 100644
index 00000000..87466bc7
--- /dev/null
+++ b/cmd/populatepipelines/populate_pipelines.go
@@ -0,0 +1,242 @@
+package main
+
+import (
+ "database/sql"
+ "flag"
+ "fmt"
+ "log"
+ "math/rand"
+ "time"
+
+ "github.com/bluesky-social/indigo/atproto/syntax"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+var (
+ dbPath = flag.String("db", "appview.db", "Path to SQLite database")
+ count = flag.Int("count", 10, "Number of pipeline runs to generate")
+ repo = flag.String("repo", "", "Repository name (e.g., 'did:plc:xyz/myrepo')")
+ knot = flag.String("knot", "localhost:8100", "Knot hostname")
+)
+
+// StatusKind represents the status of a workflow
+type StatusKind string
+
+const (
+ StatusKindPending StatusKind = "pending"
+ StatusKindRunning StatusKind = "running"
+ StatusKindFailed StatusKind = "failed"
+ StatusKindTimeout StatusKind = "timeout"
+ StatusKindCancelled StatusKind = "cancelled"
+ StatusKindSuccess StatusKind = "success"
+)
+
+var finishStatuses = []StatusKind{
+ StatusKindFailed,
+ StatusKindTimeout,
+ StatusKindCancelled,
+ StatusKindSuccess,
+}
+
+// generateRandomSha generates a random 40-character SHA
+func generateRandomSha() string {
+ const hexChars = "0123456789abcdef"
+ sha := make([]byte, 40)
+ for i := range sha {
+ sha[i] = hexChars[rand.Intn(len(hexChars))]
+ }
+ return string(sha)
+}
+
+// generateRkey generates a TID-like rkey
+func generateRkey() string {
+ // Simple timestamp-based rkey
+ now := time.Now().UnixMicro()
+ return fmt.Sprintf("%d", now)
+}
+
+func main() {
+ flag.Parse()
+
+ if *repo == "" {
+ log.Fatal("--repo is required (format: did:plc:xyz/reponame)")
+ }
+
+ // Parse repo into owner and name
+ did, repoName, ok := parseRepo(*repo)
+ if !ok {
+ log.Fatalf("Invalid repo format: %s (expected: did:plc:xyz/reponame)", *repo)
+ }
+
+ db, err := sql.Open("sqlite3", *dbPath)
+ if err != nil {
+ log.Fatalf("Failed to open database: %v", err)
+ }
+ defer db.Close()
+
+ rand.Seed(time.Now().UnixNano())
+
+ branches := []string{"main", "develop", "feature/auth", "fix/bugs"}
+ workflows := []string{"test", "build", "lint", "deploy"}
+
+ log.Printf("Generating %d pipeline runs for %s...\n", *count, *repo)
+
+ for i := 0; i < *count; i++ {
+ // Random trigger type
+ isPush := rand.Float32() > 0.3 // 70% push, 30% PR
+
+ var triggerId int64
+ if isPush {
+ triggerId, err = createPushTrigger(db, branches)
+ } else {
+ triggerId, err = createPRTrigger(db, branches)
+ }
+ if err != nil {
+ log.Fatalf("Failed to create trigger: %v", err)
+ }
+
+ // Create pipeline
+ pipelineRkey := generateRkey()
+ sha := generateRandomSha()
+ createdTime := time.Now().Add(-time.Duration(rand.Intn(7*24*60)) * time.Minute) // Random time in last week
+
+ _, err = db.Exec(`
+ INSERT INTO pipelines (knot, rkey, repo_owner, repo_name, sha, created, trigger_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, *knot, pipelineRkey, did, repoName, sha, createdTime.Format(time.RFC3339), triggerId)
+
+ if err != nil {
+ log.Fatalf("Failed to create pipeline: %v", err)
+ }
+
+ // Create workflow statuses
+ numWorkflows := rand.Intn(len(workflows)-1) + 2 // 2-4 workflows
+ selectedWorkflows := make([]string, numWorkflows)
+ perm := rand.Perm(len(workflows))
+ for j := 0; j < numWorkflows; j++ {
+ selectedWorkflows[j] = workflows[perm[j]]
+ }
+
+ for _, workflow := range selectedWorkflows {
+ err = createWorkflowStatuses(db, *knot, pipelineRkey, workflow, createdTime)
+ if err != nil {
+ log.Fatalf("Failed to create workflow statuses: %v", err)
+ }
+ }
+
+ log.Printf("Created pipeline %d/%d (rkey: %s)\n", i+1, *count, pipelineRkey)
+
+ // Small delay to ensure unique rkeys
+ time.Sleep(2 * time.Millisecond)
+ }
+
+ log.Println("✓ Pipeline population complete!")
+}
+
+func parseRepo(repo string) (syntax.DID, string, bool) {
+ // Simple parser for "did:plc:xyz/reponame"
+ for i := 0; i < len(repo); i++ {
+ if repo[i] == '/' {
+ did := syntax.DID(repo[:i])
+ name := repo[i+1:]
+ if did != "" && name != "" {
+ return did, name, true
+ }
+ }
+ }
+ return "", "", false
+}
+
+func createPushTrigger(db *sql.DB, branches []string) (int64, error) {
+ branch := branches[rand.Intn(len(branches))]
+ oldSha := generateRandomSha()
+ newSha := generateRandomSha()
+
+ result, err := db.Exec(`
+ INSERT INTO triggers (kind, push_ref, push_new_sha, push_old_sha)
+ VALUES (?, ?, ?, ?)
+ `, "push", "refs/heads/"+branch, newSha, oldSha)
+
+ if err != nil {
+ return 0, err
+ }
+
+ return result.LastInsertId()
+}
+
+func createPRTrigger(db *sql.DB, branches []string) (int64, error) {
+ targetBranch := branches[0] // Usually main
+ sourceBranch := branches[rand.Intn(len(branches)-1)+1]
+ sourceSha := generateRandomSha()
+ actions := []string{"opened", "synchronize", "reopened"}
+ action := actions[rand.Intn(len(actions))]
+
+ result, err := db.Exec(`
+ INSERT INTO triggers (kind, pr_source_branch, pr_target_branch, pr_source_sha, pr_action)
+ VALUES (?, ?, ?, ?, ?)
+ `, "pull_request", sourceBranch, targetBranch, sourceSha, action)
+
+ if err != nil {
+ return 0, err
+ }
+
+ return result.LastInsertId()
+}
+
+func createWorkflowStatuses(db *sql.DB, knot, pipelineRkey, workflow string, startTime time.Time) error {
+ // Generate a progression of statuses for the workflow
+ statusProgression := []StatusKind{StatusKindPending, StatusKindRunning}
+
+ // Randomly choose a final status (80% success, 10% failed, 5% timeout, 5% cancelled)
+ roll := rand.Float32()
+ var finalStatus StatusKind
+ switch {
+ case roll < 0.80:
+ finalStatus = StatusKindSuccess
+ case roll < 0.90:
+ finalStatus = StatusKindFailed
+ case roll < 0.95:
+ finalStatus = StatusKindTimeout
+ default:
+ finalStatus = StatusKindCancelled
+ }
+
+ statusProgression = append(statusProgression, finalStatus)
+
+ currentTime := startTime
+ for i, status := range statusProgression {
+ rkey := fmt.Sprintf("%s-%s-%d", pipelineRkey, workflow, i)
+
+ // Add some realistic time progression (10-60 seconds between statuses)
+ if i > 0 {
+ currentTime = currentTime.Add(time.Duration(rand.Intn(50)+10) * time.Second)
+ }
+
+ var errorMsg *string
+ var exitCode int
+
+ if status == StatusKindFailed {
+ msg := "Command exited with non-zero status"
+ errorMsg = &msg
+ exitCode = rand.Intn(100) + 1
+ } else if status == StatusKindTimeout {
+ msg := "Workflow exceeded maximum execution time"
+ errorMsg = &msg
+ exitCode = 124
+ }
+
+ _, err := db.Exec(`
+ INSERT INTO pipeline_statuses (
+ spindle, rkey, pipeline_knot, pipeline_rkey,
+ created, workflow, status, error, exit_code
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `, "spindle.example.com", rkey, knot, pipelineRkey,
+ currentTime.Format(time.RFC3339), workflow, string(status), errorMsg, exitCode)
+
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
--
2.51.2
From 5dce474eebfff7833a1031f1e171bf1fa3cfa64c Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Wed, 28 Jan 2026 20:30:52 +0200
Subject: [PATCH 044/122] appview: rework pipelines listing
Improve the general layout and styles.
Signed-off-by: Anirudh Oppiliappan
---
.../fragments/pipelineSymbolLong.html | 24 ++-
.../repo/pipelines/fragments/tooltip.html | 6 +-
.../templates/repo/pipelines/pipelines.html | 181 ++++++++++--------
3 files changed, 119 insertions(+), 92 deletions(-)
diff --git a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
index 80b8bb86..b6e04742 100644
--- a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
+++ b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
@@ -1,12 +1,24 @@
{{ define "repo/pipelines/fragments/pipelineSymbolLong" }}
{{ $pipeline := .Pipeline }}
{{ $repoinfo := .RepoInfo }}
+ {{ $popoverId := printf "pipeline-status-%d" $pipeline.Id }}
+
-
-
- {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }}
-
+
+ {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }}
+
+
+
{{ template "repo/pipelines/fragments/tooltip" $ }}
-
+
-{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/appview/pages/templates/repo/pipelines/fragments/tooltip.html b/appview/pages/templates/repo/pipelines/fragments/tooltip.html
index 1b2c9a94..b3e60699 100644
--- a/appview/pages/templates/repo/pipelines/fragments/tooltip.html
+++ b/appview/pages/templates/repo/pipelines/fragments/tooltip.html
@@ -2,10 +2,9 @@
{{ $repoinfo := .RepoInfo }}
{{ $pipeline := .Pipeline }}
{{ $id := $pipeline.Id }}
-
-
{{ end }}
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
index 7298d04e..1ac2bc6f 100644
--- a/appview/pages/templates/repo/pipelines/pipelines.html
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -7,108 +7,125 @@
{{ end }}
{{ define "repoContent" }}
-
-
- {{ range .Pipelines }}
- {{ block "pipeline" (list $ .) }} {{ end }}
- {{ else }}
-
-
- No pipelines have been run for this repository yet. To get started:
-
- {{ $bullet := "mx-2 text-xs bg-gray-200 dark:bg-gray-600 rounded-full size-5 flex items-center justify-center font-mono inline-flex align-middle" }}
-
- 1 First, choose a spindle in your
- repository settings .
-
-
- 2 Configure your CI/CD
- pipeline .
-
-
3 Trigger a workflow with a push or a pull-request!
-
- {{ end }}
+
+
+ {{ len .Pipelines }} pipeline run{{ if ne (len .Pipelines) 1 }}s{{ end }}
+
{{ end }}
+{{ define "repoAfter" }}
+{{ if .Pipelines }}
+
+ {{ range .Pipelines }}
+ {{ template "pipelineCard" (dict "Root" $ "Pipeline" .) }}
+ {{ end }}
+
+{{ else }}
+
+
+ {{ i "package" "size-16 text-gray-300 dark:text-gray-700" }}
+
+
+
+
+ No pipelines have been run yet
+
+
+ Get started by configuring CI/CD for this repository
+
+
-{{ define "pipeline" }}
- {{ $root := index . 0 }}
- {{ $p := index . 1 }}
-
- {{ block "pipelineHeader" $ }} {{ end }}
+
+
+
+
2
+
+ Configure your CI/CD
+ pipeline
+
+
+
+
3
+
+ Trigger a workflow with a push or pull request
+
+
+
{{ end }}
+{{ end }}
-{{ define "pipelineHeader" }}
- {{ $root := index . 0 }}
- {{ $p := index . 1 }}
+{{ define "pipelineCard" }}
+ {{ $root := .Root }}
+ {{ $p := .Pipeline }}
{{ with $p }}
-
-
- {{ .Trigger.Kind.String }}
-
+
+
+
+
-
- {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" . "RepoInfo" $root.RepoInfo) }}
+
+
+
+
-
- {{ template "repo/fragments/shortTimeAgo" .Created }}
+
+
+ {{ template "repo/fragments/time" .Created }}
- {{ $t := .TimeTaken }}
-
+
+
+ {{ $t := .TimeTaken }}
{{ if $t }}
- {{ $t | durationFmt }}
+
+ {{ i "clock" "size-3" }}
+ {{ $t | durationFmt }}
+
{{ else }}
- --
- {{ end }}
-
-
-
-
+
+
{{ end }}
{{ end }}
--
2.51.2
From ef3dbf02ea0d133dffb20583ac15c761951b9c8f Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Sun, 8 Feb 2026 15:22:18 +0200
Subject: [PATCH 045/122] appview/{pipelines,pages}: filter pipelines list by
push/PR
Signed-off-by: Anirudh Oppiliappan
---
appview/pages/pages.go | 10 +++--
.../templates/repo/pipelines/pipelines.html | 39 ++++++++++++++++---
appview/pipelines/pipelines.go | 24 ++++++++++--
3 files changed, 61 insertions(+), 12 deletions(-)
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 813ab2f4..c7fa9ff9 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -1348,10 +1348,12 @@ func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
}
type PipelinesParams struct {
- LoggedInUser *oauth.MultiAccountUser
- RepoInfo repoinfo.RepoInfo
- Pipelines []models.Pipeline
- Active string
+ LoggedInUser *oauth.MultiAccountUser
+ RepoInfo repoinfo.RepoInfo
+ Pipelines []models.Pipeline
+ Active string
+ FilteringByPush bool
+ FilteringByPR bool
}
func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
index 1ac2bc6f..5d02564d 100644
--- a/appview/pages/templates/repo/pipelines/pipelines.html
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -7,12 +7,41 @@
{{ end }}
{{ define "repoContent" }}
-
-
- {{ len .Pipelines }} pipeline run{{ if ne (len .Pipelines) 1 }}s{{ end }}
+ {{ $active := "all" }}
+ {{ if .FilteringByPush }}
+ {{ $active = "push" }}
+ {{ else if .FilteringByPR }}
+ {{ $active = "pr" }}
+ {{ end }}
+
+ {{ $all :=
+ (dict
+ "Key" "all"
+ "Value" "all"
+ "Icon" "package"
+ "Meta" "") }}
+ {{ $push :=
+ (dict
+ "Key" "push"
+ "Value" "push"
+ "Icon" "git-commit-horizontal"
+ "Meta" "") }}
+ {{ $pr :=
+ (dict
+ "Key" "pr"
+ "Value" "pull request"
+ "Icon" "git-pull-request"
+ "Meta" "") }}
+ {{ $values := list $all $push $pr }}
+
+
+
+ {{ template "fragments/tabSelector" (dict "Name" "trigger" "Values" $values "Active" $active) }}
+
+
+ {{ len .Pipelines }} pipeline run{{ if ne (len .Pipelines) 1 }}s{{ end }}
+
-
-
{{ end }}
{{ define "repoAfter" }}
diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go
index 8194354e..a3d36fe0 100644
--- a/appview/pipelines/pipelines.go
+++ b/appview/pipelines/pipelines.go
@@ -98,10 +98,28 @@ func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
return
}
+ // Filter by trigger
+ filterTrigger := r.URL.Query().Get("trigger")
+ var filtered []models.Pipeline
+ for _, pipeline := range ps {
+ if filterTrigger == "push" && pipeline.Trigger != nil && pipeline.Trigger.IsPush() {
+ filtered = append(filtered, pipeline)
+ } else if filterTrigger == "pr" && pipeline.Trigger != nil && pipeline.Trigger.IsPullRequest() {
+ filtered = append(filtered, pipeline)
+ } else if filterTrigger == "" || filterTrigger == "all" {
+ filtered = append(filtered, pipeline)
+ }
+ }
+
+ filteringByPush := filterTrigger == "push"
+ filteringByPR := filterTrigger == "pr"
+
p.pages.Pipelines(w, pages.PipelinesParams{
- LoggedInUser: user,
- RepoInfo: p.repoResolver.GetRepoInfo(r, user),
- Pipelines: ps,
+ LoggedInUser: user,
+ RepoInfo: p.repoResolver.GetRepoInfo(r, user),
+ Pipelines: filtered,
+ FilteringByPush: filteringByPush,
+ FilteringByPR: filteringByPR,
})
}
--
2.51.2
From b0d4690d3c26f8b272a5ad299fea52b84d3df4fa Mon Sep 17 00:00:00 2001
From: oppiliappan
Date: Mon, 9 Feb 2026 04:21:59 +0000
Subject: [PATCH 046/122] appview/pipelines: fix incorrect totals
the default query limits to 30 items, we need a separate query for
total pipeline counts.
Signed-off-by: oppiliappan
---
appview/db/pipeline.go | 49 ++++++++++++-
appview/pages/pages.go | 12 ++--
.../templates/repo/pipelines/pipelines.html | 15 ++--
appview/pipelines/pipelines.go | 72 ++++++++++---------
appview/pulls/pulls.go | 16 ++---
appview/repo/repo_util.go | 8 +--
6 files changed, 109 insertions(+), 63 deletions(-)
diff --git a/appview/db/pipeline.go b/appview/db/pipeline.go
index bd5e19c3..b643628c 100644
--- a/appview/db/pipeline.go
+++ b/appview/db/pipeline.go
@@ -170,11 +170,13 @@ func AddPipelineStatus(e Execer, status models.PipelineStatus) error {
// this is a mega query, but the most useful one:
// get N pipelines, for each one get the latest status of its N workflows
+//
+// the pipelines table is aliased to `p`
+// the triggers table is aliased to `t`
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
var conditions []string
var args []any
for _, filter := range filters {
- filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
conditions = append(conditions, filter.Condition())
args = append(args, filter.Arg()...)
}
@@ -366,3 +368,48 @@ func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.P
return all, nil
}
+
+// the pipelines table is aliased to `p`
+// the triggers table is aliased to `t`
+func GetTotalPipelineStatuses(e Execer, filters ...orm.Filter) (int64, error) {
+ var conditions []string
+ var args []any
+ for _, filter := range filters {
+ conditions = append(conditions, filter.Condition())
+ args = append(args, filter.Arg()...)
+ }
+
+ whereClause := ""
+ if conditions != nil {
+ whereClause = " where " + strings.Join(conditions, " and ")
+ }
+
+ query := fmt.Sprintf(`
+ select
+ count(1)
+ from
+ pipelines p
+ join
+ triggers t ON p.trigger_id = t.id
+ %s
+ `, whereClause)
+
+ rows, err := e.Query(query, args...)
+ if err != nil {
+ return 0, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var count int64
+ err := rows.Scan(&count)
+ if err != nil {
+ return 0, err
+ }
+
+ return count, nil
+ }
+
+ // unreachable
+ return 0, nil
+}
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index c7fa9ff9..0b758f31 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -1348,12 +1348,12 @@ func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
}
type PipelinesParams struct {
- LoggedInUser *oauth.MultiAccountUser
- RepoInfo repoinfo.RepoInfo
- Pipelines []models.Pipeline
- Active string
- FilteringByPush bool
- FilteringByPR bool
+ LoggedInUser *oauth.MultiAccountUser
+ RepoInfo repoinfo.RepoInfo
+ Pipelines []models.Pipeline
+ Active string
+ FilterKind string
+ Total int64
}
func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
index 5d02564d..0a51048f 100644
--- a/appview/pages/templates/repo/pipelines/pipelines.html
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -7,12 +7,7 @@
{{ end }}
{{ define "repoContent" }}
- {{ $active := "all" }}
- {{ if .FilteringByPush }}
- {{ $active = "push" }}
- {{ else if .FilteringByPR }}
- {{ $active = "pr" }}
- {{ end }}
+ {{ $active := .FilterKind }}
{{ $all :=
(dict
@@ -28,7 +23,7 @@
"Meta" "") }}
{{ $pr :=
(dict
- "Key" "pr"
+ "Key" "pull_request"
"Value" "pull request"
"Icon" "git-pull-request"
"Meta" "") }}
@@ -36,10 +31,10 @@
- {{ template "fragments/tabSelector" (dict "Name" "trigger" "Values" $values "Active" $active) }}
+ {{ template "fragments/tabSelector" (dict "Name" "trigger" "Values" $values "Active" .FilterKind) }}
- {{ len .Pipelines }} pipeline run{{ if ne (len .Pipelines) 1 }}s{{ end }}
+ {{ .Total }} pipeline run{{ if ne .Total 1 }}s{{ end }}
{{ end }}
@@ -52,7 +47,7 @@
{{ end }}
{{ else }}
-
+
{{ i "package" "size-16 text-gray-300 dark:text-gray-700" }}
diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go
index a3d36fe0..094c40bc 100644
--- a/appview/pipelines/pipelines.go
+++ b/appview/pipelines/pipelines.go
@@ -86,40 +86,44 @@ func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
return
}
+ filterKind := r.URL.Query().Get("trigger")
+ filters := []orm.Filter{
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ }
+ switch filterKind {
+ case "push":
+ filters = append(filters, orm.FilterEq("t.kind", "push"))
+ case "pull_request":
+ filters = append(filters, orm.FilterEq("t.kind", "pull_request"))
+ default:
+ // no filters otherwise, default to "all"
+ filterKind = "all"
+ }
+
ps, err := db.GetPipelineStatuses(
p.db,
30,
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
+ filters...,
)
if err != nil {
l.Error("failed to query db", "err", err)
return
}
- // Filter by trigger
- filterTrigger := r.URL.Query().Get("trigger")
- var filtered []models.Pipeline
- for _, pipeline := range ps {
- if filterTrigger == "push" && pipeline.Trigger != nil && pipeline.Trigger.IsPush() {
- filtered = append(filtered, pipeline)
- } else if filterTrigger == "pr" && pipeline.Trigger != nil && pipeline.Trigger.IsPullRequest() {
- filtered = append(filtered, pipeline)
- } else if filterTrigger == "" || filterTrigger == "all" {
- filtered = append(filtered, pipeline)
- }
+ total, err := db.GetTotalPipelineStatuses(p.db, filters...)
+ if err != nil {
+ l.Error("failed to query db", "err", err)
+ return
}
- filteringByPush := filterTrigger == "push"
- filteringByPR := filterTrigger == "pr"
-
p.pages.Pipelines(w, pages.PipelinesParams{
- LoggedInUser: user,
- RepoInfo: p.repoResolver.GetRepoInfo(r, user),
- Pipelines: filtered,
- FilteringByPush: filteringByPush,
- FilteringByPR: filteringByPR,
+ LoggedInUser: user,
+ RepoInfo: p.repoResolver.GetRepoInfo(r, user),
+ Pipelines: ps,
+ FilterKind: filterKind,
+ Total: total,
})
}
@@ -148,10 +152,10 @@ func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
ps, err := db.GetPipelineStatuses(
p.db,
1,
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
- orm.FilterEq("id", pipelineId),
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ orm.FilterEq("p.id", pipelineId),
)
if err != nil {
l.Error("failed to query db", "err", err)
@@ -215,10 +219,10 @@ func (p *Pipelines) Logs(w http.ResponseWriter, r *http.Request) {
ps, err := db.GetPipelineStatuses(
p.db,
1,
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
- orm.FilterEq("id", pipelineId),
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ orm.FilterEq("p.id", pipelineId),
)
if err != nil || len(ps) != 1 {
l.Error("pipeline query failed", "err", err, "count", len(ps))
@@ -364,10 +368,10 @@ func (p *Pipelines) Cancel(w http.ResponseWriter, r *http.Request) {
ps, err := db.GetPipelineStatuses(
p.db,
1,
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
- orm.FilterEq("id", pipelineId),
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ orm.FilterEq("p.id", pipelineId),
)
if err != nil {
return models.Pipeline{}, err
diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go
index 441a9dd7..f037636b 100644
--- a/appview/pulls/pulls.go
+++ b/appview/pulls/pulls.go
@@ -214,10 +214,10 @@ func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff
ps, err := db.GetPipelineStatuses(
s.db,
len(shas),
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
- orm.FilterIn("sha", shas),
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ orm.FilterIn("p.sha", shas),
)
if err != nil {
log.Printf("failed to fetch pipeline statuses: %s", err)
@@ -636,10 +636,10 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
ps, err := db.GetPipelineStatuses(
s.db,
len(shas),
- orm.FilterEq("repo_owner", f.Did),
- orm.FilterEq("repo_name", f.Name),
- orm.FilterEq("knot", f.Knot),
- orm.FilterIn("sha", shas),
+ orm.FilterEq("p.repo_owner", f.Did),
+ orm.FilterEq("p.repo_name", f.Name),
+ orm.FilterEq("p.knot", f.Knot),
+ orm.FilterIn("p.sha", shas),
)
if err != nil {
log.Printf("failed to fetch pipeline statuses: %s", err)
diff --git a/appview/repo/repo_util.go b/appview/repo/repo_util.go
index 7d40fc67..8bf020b8 100644
--- a/appview/repo/repo_util.go
+++ b/appview/repo/repo_util.go
@@ -103,10 +103,10 @@ func getPipelineStatuses(
ps, err := db.GetPipelineStatuses(
d,
len(shas),
- orm.FilterEq("repo_owner", repo.Did),
- orm.FilterEq("repo_name", repo.Name),
- orm.FilterEq("knot", repo.Knot),
- orm.FilterIn("sha", shas),
+ orm.FilterEq("p.repo_owner", repo.Did),
+ orm.FilterEq("p.repo_name", repo.Name),
+ orm.FilterEq("p.knot", repo.Knot),
+ orm.FilterIn("p.sha", shas),
)
if err != nil {
return nil, err
--
2.51.2
From f569d2beae25b937addee796bcffd68be36171ec Mon Sep 17 00:00:00 2001
From: Lewis
Date: Wed, 4 Feb 2026 16:24:56 +0100
Subject: [PATCH 047/122] appview/profile: show dummy profile when no tangled
profile
---
appview/db/profile.go | 4 +---
appview/oauth/handler.go | 4 ++--
appview/pages/pages.go | 1 +
.../pages/templates/layouts/profilebase.html | 13 +++++++++++
appview/state/profile.go | 22 +++++++++++++++++++
appview/state/state.go | 2 +-
6 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/appview/db/profile.go b/appview/db/profile.go
index 7ffc4cc3..fdc5d993 100644
--- a/appview/db/profile.go
+++ b/appview/db/profile.go
@@ -360,9 +360,7 @@ func GetProfile(e Execer, did string) (*models.Profile, error) {
did,
).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns)
if err == sql.ErrNoRows {
- profile := models.Profile{}
- profile.Did = did
- return &profile, nil
+ return nil, nil
}
if err != nil {
diff --git a/appview/oauth/handler.go b/appview/oauth/handler.go
index 5f23ac65..33d8b04f 100644
--- a/appview/oauth/handler.go
+++ b/appview/oauth/handler.go
@@ -199,8 +199,8 @@ func (o *OAuth) ensureTangledProfile(sessData *oauth.ClientSessionData) {
did := sessData.AccountDID.String()
l := o.Logger.With("did", did)
- _, err := db.GetProfile(o.Db, did)
- if err == nil {
+ profile, _ := db.GetProfile(o.Db, did)
+ if profile != nil {
l.Debug("profile already exists in DB")
return
}
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 0b758f31..fa4c0be5 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -523,6 +523,7 @@ func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
type ProfileCard struct {
UserDid string
+ HasProfile bool
FollowStatus models.FollowStatus
Punchcard *models.Punchcard
Profile *models.Profile
diff --git a/appview/pages/templates/layouts/profilebase.html b/appview/pages/templates/layouts/profilebase.html
index 7aeb4937..3c577865 100644
--- a/appview/pages/templates/layouts/profilebase.html
+++ b/appview/pages/templates/layouts/profilebase.html
@@ -18,6 +18,18 @@
{{ end }}
{{ define "content" }}
+ {{ if not .Card.HasProfile }}
+
+
+
+
+
{{ resolve .Card.UserDid }}
+
This user hasn't joined Tangled yet.
+
Let them know we're waiting for them!
+
+
+
+ {{ else }}
{{ template "profileTabs" . }}
@@ -35,6 +47,7 @@
{{ block "profileContent" . }} {{ end }}
+ {{ end }}
{{ end }}
{{ define "profileTabs" }}
diff --git a/appview/state/profile.go b/appview/state/profile.go
index 118306b0..cfdd2713 100644
--- a/appview/state/profile.go
+++ b/appview/state/profile.go
@@ -58,6 +58,11 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) {
return nil, fmt.Errorf("failed to get profile: %w", err)
}
+ hasProfile := profile != nil
+ if !hasProfile {
+ profile = &models.Profile{Did: did}
+ }
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
if err != nil {
return nil, fmt.Errorf("failed to get repo count: %w", err)
@@ -98,6 +103,7 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) {
return &pages.ProfileCard{
UserDid: did,
+ HasProfile: hasProfile,
Profile: profile,
FollowStatus: followStatus,
Stats: pages.ProfileStats{
@@ -533,6 +539,9 @@ func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
}
+ if profile == nil {
+ profile = &models.Profile{Did: user.Active.Did}
+ }
profile.Description = r.FormValue("description")
profile.IncludeBluesky = r.FormValue("includeBluesky") == "on"
@@ -576,6 +585,9 @@ func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
}
+ if profile == nil {
+ profile = &models.Profile{Did: user.Active.Did}
+ }
i := 0
var pinnedRepos [6]syntax.ATURI
@@ -676,6 +688,9 @@ func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
}
+ if profile == nil {
+ profile = &models.Profile{Did: user.Active.Did}
+ }
s.pages.EditBioFragment(w, pages.EditBioParams{
LoggedInUser: user,
@@ -690,6 +705,9 @@ func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Printf("getting profile data for %s: %s", user.Active.Did, err)
}
+ if profile == nil {
+ profile = &models.Profile{Did: user.Active.Did}
+ }
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did))
if err != nil {
@@ -816,6 +834,8 @@ func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) {
profile, err := db.GetProfile(s.db, user.Did)
if err != nil {
l.Warn("getting profile data from DB", "err", err)
+ }
+ if profile == nil {
profile = &models.Profile{Did: user.Did}
}
profile.Avatar = uploadBlobResp.Blob.Ref.String()
@@ -892,6 +912,8 @@ func (s *State) RemoveProfileAvatar(w http.ResponseWriter, r *http.Request) {
profile, err := db.GetProfile(s.db, user.Did)
if err != nil {
l.Warn("getting profile data from DB", "err", err)
+ }
+ if profile == nil {
profile = &models.Profile{Did: user.Did}
}
profile.Avatar = ""
diff --git a/appview/state/state.go b/appview/state/state.go
index 4da48f08..1baa3985 100644
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -126,7 +126,7 @@ func Make(ctx context.Context, config *config.Config) (*State, error) {
wrapper,
false,
- // in-memory filter is inapplicalble to appview so
+ // in-memory filter is inapplicable to appview so
// we'll never log dids anyway.
false,
)
--
2.51.2
From 37a9708f60897038038bf16020ed0f14dcbb2878 Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Mon, 9 Feb 2026 15:39:54 +0200
Subject: [PATCH 048/122] appview/state: update robots.txt
Signed-off-by: Anirudh Oppiliappan
---
appview/state/state.go | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/appview/state/state.go b/appview/state/state.go
index 1baa3985..5877e866 100644
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -207,8 +207,15 @@ func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "public, max-age=86400") // one day
- robotsTxt := `User-agent: *
+ robotsTxt := `# Hello, Tanglers!
+User-agent: *
Allow: /
+Disallow: /*/*/settings
+Disallow: /settings
+Disallow: /*/*/compare
+Disallow: /*/*/fork
+
+Crawl-delay: 1
`
w.Write([]byte(robotsTxt))
}
--
2.51.2
From e17836203f86a090a9f663d54f3cde82fb57a29d Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Mon, 9 Feb 2026 15:39:54 +0200
Subject: [PATCH 049/122] appview/pages: improved seo tags for home, repo and
profile
Signed-off-by: Anirudh Oppiliappan
---
appview/pages/templates/layouts/base.html | 17 ++++++++++++--
.../pages/templates/layouts/profilebase.html | 20 +++++++++++++----
.../pages/templates/repo/fragments/og.html | 19 ++++++++++++++--
appview/pages/templates/timeline/home.html | 22 ++++++++++++++++---
4 files changed, 67 insertions(+), 11 deletions(-)
diff --git a/appview/pages/templates/layouts/base.html b/appview/pages/templates/layouts/base.html
index 9a81126c..07d995d7 100644
--- a/appview/pages/templates/layouts/base.html
+++ b/appview/pages/templates/layouts/base.html
@@ -4,9 +4,22 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -26,7 +39,7 @@
- {{ block "title" . }}{{ end }} · tangled
+ {{ block "title" . }}{{ end }}
{{ block "extrameta" . }}{{ end }}
diff --git a/appview/pages/templates/layouts/profilebase.html b/appview/pages/templates/layouts/profilebase.html
index 3c577865..30ee9622 100644
--- a/appview/pages/templates/layouts/profilebase.html
+++ b/appview/pages/templates/layouts/profilebase.html
@@ -3,18 +3,30 @@
{{ define "extrameta" }}
{{ $handle := resolve .Card.UserDid }}
{{ $avatarUrl := profileAvatarUrl .Card.Profile "" }}
+ {{ $description := or .Card.Profile.Description (printf "%s on Tangled" $handle) }}
+ {{ $url := printf "https://tangled.org/%s" $handle }}
+
+
-
-
+
+
-
+
+
+
+
-
+
+
+
+
+
+
{{ end }}
{{ define "content" }}
diff --git a/appview/pages/templates/repo/fragments/og.html b/appview/pages/templates/repo/fragments/og.html
index ead2132d..968a7500 100644
--- a/appview/pages/templates/repo/fragments/og.html
+++ b/appview/pages/templates/repo/fragments/og.html
@@ -1,19 +1,34 @@
{{ define "repo/fragments/og" }}
{{ $title := or .Title .RepoInfo.FullName }}
- {{ $description := or .Description .RepoInfo.Description }}
+ {{ $description := or .Description .RepoInfo.Description "A repository on Tangled" }}
{{ $url := or .Url (printf "https://tangled.org/%s" .RepoInfo.FullName) }}
{{ $imageUrl := printf "https://tangled.org/%s/opengraph" .RepoInfo.FullName }}
+ {{ $ownerHandle := resolve .RepoInfo.OwnerDid }}
+
-
+
+
+
+ {{ if .RepoInfo.Topics }}
+ {{ range .RepoInfo.Topics }}
+
+ {{ end }}
+ {{ end }}
+
+
+
+
+
+
{{ end }}
diff --git a/appview/pages/templates/timeline/home.html b/appview/pages/templates/timeline/home.html
index febf520e..995d18fa 100644
--- a/appview/pages/templates/timeline/home.html
+++ b/appview/pages/templates/timeline/home.html
@@ -1,10 +1,26 @@
{{ define "title" }}tangled · tightly-knit social coding{{ end }}
{{ define "extrameta" }}
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ end }}
--
2.51.2
From 49f269374a464eebc032280ca7e217457ad44e7b Mon Sep 17 00:00:00 2001
From: "pdewey.com"
Date: Sat, 7 Feb 2026 18:45:21 -0500
Subject: [PATCH 050/122] appview/pulls: fix search not updating count of pull
requests
searching for pull requests did not previously update open/merged/closed
pull requests counts https://tangled.org/tangled.org/core/issues/400.
Signed-off-by: pdewey.com
---
appview/pulls/pulls.go | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go
index f037636b..6a55157f 100644
--- a/appview/pulls/pulls.go
+++ b/appview/pulls/pulls.go
@@ -553,6 +553,8 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
keyword := params.Get("q")
+ repoInfo := s.repoResolver.GetRepoInfo(r, user)
+
var pulls []*models.Pull
searchOpts := models.PullSearchOptions{
Keyword: keyword,
@@ -570,6 +572,36 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
totalPulls = int(res.Total)
l.Debug("searched pulls with indexer", "count", len(res.Hits))
+ // count matching pulls in the other states to display correct counts
+ for _, other := range []models.PullState{models.PullOpen, models.PullMerged, models.PullClosed} {
+ if other == state {
+ continue
+ }
+ countRes, err := s.indexer.Search(r.Context(), models.PullSearchOptions{
+ Keyword: keyword, RepoAt: f.RepoAt().String(), State: other,
+ Page: pagination.Page{Limit: 1},
+ })
+ if err != nil {
+ continue
+ }
+ switch other {
+ case models.PullOpen:
+ repoInfo.Stats.PullCount.Open = int(countRes.Total)
+ case models.PullMerged:
+ repoInfo.Stats.PullCount.Merged = int(countRes.Total)
+ case models.PullClosed:
+ repoInfo.Stats.PullCount.Closed = int(countRes.Total)
+ }
+ }
+ switch state {
+ case models.PullOpen:
+ repoInfo.Stats.PullCount.Open = int(res.Total)
+ case models.PullMerged:
+ repoInfo.Stats.PullCount.Merged = int(res.Total)
+ case models.PullClosed:
+ repoInfo.Stats.PullCount.Closed = int(res.Total)
+ }
+
pulls, err = db.GetPulls(
s.db,
orm.FilterIn("id", res.Hits),
@@ -668,7 +700,7 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) {
s.pages.RepoPulls(w, pages.RepoPullsParams{
LoggedInUser: s.oauth.GetMultiAccountUser(r),
- RepoInfo: s.repoResolver.GetRepoInfo(r, user),
+ RepoInfo: repoInfo,
Pulls: pulls,
LabelDefs: defs,
FilteringBy: state,
--
2.51.2
From e11751cd3366e288734165632af69f7bd32c96dd Mon Sep 17 00:00:00 2001
From: moshyfawn
Date: Tue, 10 Feb 2026 14:41:46 -0500
Subject: [PATCH 051/122] spindle/nixery: update setup command docs
Signed-off-by: moshyfawn
---
spindle/engines/nixery/setup_steps.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spindle/engines/nixery/setup_steps.go b/spindle/engines/nixery/setup_steps.go
index 8c619478..ff58e7a0 100644
--- a/spindle/engines/nixery/setup_steps.go
+++ b/spindle/engines/nixery/setup_steps.go
@@ -17,7 +17,7 @@ echo 'build-users-group = ' >> /etc/nix/nix.conf`
// dependencyStep processes dependencies defined in the workflow.
// For dependencies using a custom registry (i.e. not nixpkgs), it collects
-// all packages and adds a single 'nix profile install' step to the
+// all packages and adds a single 'nix profile add' step to the
// beginning of the workflow's step list.
func dependencyStep(deps map[string][]string) *Step {
var customPackages []string
--
2.51.2
From 2c65fbc9ccbc60851da549afd88113b1f6ad55d4 Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Fri, 13 Feb 2026 17:43:12 +0200
Subject: [PATCH 052/122] appview/pulls: check if record.Source is nil
It will be for patch pulls. Prevents a nil pointer deref when we set newSourceRev.
---
appview/pulls/pulls.go | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go
index 6a55157f..a27115ef 100644
--- a/appview/pulls/pulls.go
+++ b/appview/pulls/pulls.go
@@ -1915,7 +1915,10 @@ func (s *Pulls) resubmitPullHelper(
record := pull.AsRecord()
record.PatchBlob = blob.Blob
record.CreatedAt = time.Now().Format(time.RFC3339)
- record.Source.Sha = newSourceRev
+
+ if record.Source != nil {
+ record.Source.Sha = newSourceRev
+ }
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
--
2.51.2
From a66040a1dc59e54ea46b73928bb9ea9213feaf3f Mon Sep 17 00:00:00 2001
From: iacore
Date: Sat, 14 Feb 2026 22:56:21 +0000
Subject: [PATCH 053/122] appview/pages: make comment buttons show "hand"
cursor
Signed-off-by: iacore
---
.../pages/templates/repo/fragments/labelSectionHeader.html | 2 +-
.../templates/repo/issues/fragments/issueCommentActions.html | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/appview/pages/templates/repo/fragments/labelSectionHeader.html b/appview/pages/templates/repo/fragments/labelSectionHeader.html
index df9b4daa..4ca15727 100644
--- a/appview/pages/templates/repo/fragments/labelSectionHeader.html
+++ b/appview/pages/templates/repo/fragments/labelSectionHeader.html
@@ -4,7 +4,7 @@
{{ template "repo/fragments/labelSectionHeaderText" .Name }}
{{ if (or .RepoInfo.Roles.IsOwner .RepoInfo.Roles.IsCollaborator) }}
@@ -21,7 +21,7 @@
{{ define "delete" }}
Date: Sun, 15 Feb 2026 07:34:58 +0000
Subject: [PATCH 054/122] knotserver: add stub message for merge-checks
Signed-off-by: oppiliappan
---
knotserver/xrpc/merge_check.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/knotserver/xrpc/merge_check.go b/knotserver/xrpc/merge_check.go
index 68cd6bac..041687af 100644
--- a/knotserver/xrpc/merge_check.go
+++ b/knotserver/xrpc/merge_check.go
@@ -53,6 +53,7 @@ func (x *Xrpc) MergeCheck(w http.ResponseWriter, r *http.Request) {
}
mo := git.MergeOptions{}
+ mo.CommitMessage = "merge check"
mo.CommitterName = x.Config.Git.UserName
mo.CommitterEmail = x.Config.Git.UserEmail
mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)
--
2.51.2
From c4e5e34758373589bef7c68e7d4137cd2043ec30 Mon Sep 17 00:00:00 2001
From: Anirudh Oppiliappan
Date: Sun, 15 Feb 2026 10:01:58 +0200
Subject: [PATCH 055/122] appview/state: bump profile avatar size limit to 5mb
Signed-off-by: Anirudh Oppiliappan
---
appview/pages/templates/user/fragments/editAvatar.html | 4 ++--
appview/state/profile.go | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/appview/pages/templates/user/fragments/editAvatar.html b/appview/pages/templates/user/fragments/editAvatar.html
index ce985d39..ab82c216 100644
--- a/appview/pages/templates/user/fragments/editAvatar.html
+++ b/appview/pages/templates/user/fragments/editAvatar.html
@@ -7,7 +7,7 @@
Upload or Remove Avatar
- Upload a new image (PNG or JPEG, max 1MB) or remove your current avatar.
+ Upload a new image (PNG or JPEG, max 5MB) or remove your current avatar.
- {{ i "upload" "size-4 inline group-[.htmx-request]/form:hidden" }}
+ {{ i "upload" "size-4 inline group-[.htmx-request]/form:hidden" }}
{{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]/form:inline" }}
upload
diff --git a/appview/state/profile.go b/appview/state/profile.go
index cfdd2713..67587abd 100644
--- a/appview/state/profile.go
+++ b/appview/state/profile.go
@@ -763,9 +763,9 @@ func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) {
}
defer file.Close()
- if header.Size > 1000000 {
+ if header.Size > 5000000 {
l.Warn("avatar file too large", "size", header.Size)
- s.pages.Notice(w, "avatar-error", "Avatar file too large (max 1MB)")
+ s.pages.Notice(w, "avatar-error", "Avatar file too large (max 5MB)")
return
}
--
2.51.2
From 0ab3af52530a6e6d47120fb9f1c1ebbdb0adb911 Mon Sep 17 00:00:00 2001
From: oppiliappan
Date: Mon, 16 Feb 2026 03:31:36 +0000
Subject: [PATCH 056/122] appview: fix oauth client URI
there was a change that removed the scheme from appview host, this was
not reflected in the non-dev oauth config.
Signed-off-by: oppiliappan
---
appview/oauth/oauth.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go
index 4aa90221..6c4c105a 100644
--- a/appview/oauth/oauth.go
+++ b/appview/oauth/oauth.go
@@ -43,7 +43,7 @@ func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enf
callbackUri := clientUri + "/oauth/callback"
oauthConfig = oauth.NewLocalhostConfig(callbackUri, TangledScopes)
} else {
- clientUri = config.Core.AppviewHost
+ clientUri = "https://" + config.Core.AppviewHost
clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri)
callbackUri := clientUri + "/oauth/callback"
oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, TangledScopes)
--
2.51.2
From eb3e271982e4e6c4133a0fa35e0a208b78b22875 Mon Sep 17 00:00:00 2001
From: oppiliappan
Date: Mon, 16 Feb 2026 06:08:19 +0000
Subject: [PATCH 057/122] appview/pages: fix shrinking buttons in repo header
Signed-off-by: oppiliappan
---
appview/pages/templates/layouts/repobase.html | 144 ++++++++++--------
1 file changed, 80 insertions(+), 64 deletions(-)
diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html
index d3347664..c5d31bf5 100644
--- a/appview/pages/templates/layouts/repobase.html
+++ b/appview/pages/templates/layouts/repobase.html
@@ -2,74 +2,20 @@
{{ define "content" }}