forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+705 -157
api
tangled
appview
docs
spindle
knotserver
config
xrpc
lexicons
repo
nix
types
+10
api/tangled/repotree.go
···
Files []*RepoTree_TreeEntry `json:"files" cborgen:"files"`
// parent: The parent path in the tree
Parent *string `json:"parent,omitempty" cborgen:"parent,omitempty"`
+
// readme: Readme for this file tree
+
Readme *RepoTree_Readme `json:"readme,omitempty" cborgen:"readme,omitempty"`
// ref: The git reference used
Ref string `json:"ref" cborgen:"ref"`
}
+
// RepoTree_Readme is a "readme" in the sh.tangled.repo.tree schema.
+
type RepoTree_Readme struct {
+
// contents: Contents of the readme file
+
Contents string `json:"contents" cborgen:"contents"`
+
// filename: Name of the readme file
+
Filename string `json:"filename" cborgen:"filename"`
+
}
+
// RepoTree_TreeEntry is a "treeEntry" in the sh.tangled.repo.tree schema.
type RepoTree_TreeEntry struct {
// is_file: Whether this entry is a file
+15 -17
appview/pages/markup/format.go
···
package markup
-
import "strings"
+
import (
+
"regexp"
+
)
type Format string
···
)
var FileTypes map[Format][]string = map[Format][]string{
-
FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
+
FormatMarkdown: {".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
+
}
+
+
var FileTypePatterns = map[Format]*regexp.Regexp{
+
FormatMarkdown: regexp.MustCompile(`(?i)\.(md|markdown|mdown|mkdn|mkd)$`),
}
-
// ReadmeFilenames contains the list of common README filenames to search for,
-
// in order of preference. Only includes well-supported formats.
-
var ReadmeFilenames = []string{
-
"README.md", "readme.md",
-
"README",
-
"readme",
-
"README.markdown",
-
"readme.markdown",
-
"README.txt",
-
"readme.txt",
+
var ReadmePattern = regexp.MustCompile(`(?i)^readme(\.(md|markdown|txt))?$`)
+
+
func IsReadmeFile(filename string) bool {
+
return ReadmePattern.MatchString(filename)
}
func GetFormat(filename string) Format {
-
for format, extensions := range FileTypes {
-
for _, extension := range extensions {
-
if strings.HasSuffix(filename, extension) {
-
return format
-
}
+
for format, pattern := range FileTypePatterns {
+
if pattern.MatchString(filename) {
+
return format
}
}
// default format
+2 -2
appview/pages/templates/repo/fragments/readme.html
···
{{ define "repo/fragments/readme" }}
<div class="mt-4 rounded bg-white dark:bg-gray-800 drop-shadow-sm w-full mx-auto overflow-hidden">
{{- if .ReadmeFileName -}}
-
<div class="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2">
+
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2">
{{ i "file-text" "w-4 h-4" "text-gray-600 dark:text-gray-400" }}
<span class="font-mono text-sm text-gray-800 dark:text-gray-200">{{ .ReadmeFileName }}</span>
</div>
{{- end -}}
<section
-
class="p-6 overflow-auto {{ if not .Raw }}
+
class="px-6 pb-6 overflow-auto {{ if not .Raw }}
prose dark:prose-invert dark:[&_pre]:bg-gray-900
dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900
dark:[&_pre]:border dark:[&_pre]:border-gray-700
+24
knotserver/xrpc/repo_tree.go
···
"net/http"
"path/filepath"
"time"
+
"unicode/utf8"
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/pages/markup"
"tangled.org/core/knotserver/git"
xrpcerr "tangled.org/core/xrpc/errors"
)
···
return
}
+
// if any of these files are a readme candidate, pass along its blob contents too
+
var readmeFileName string
+
var readmeContents string
+
for _, file := range files {
+
if markup.IsReadmeFile(file.Name) {
+
contents, err := gr.RawContent(filepath.Join(path, file.Name))
+
if err != nil {
+
x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name)
+
}
+
+
if utf8.Valid(contents) {
+
readmeFileName = file.Name
+
readmeContents = string(contents)
+
break
+
}
+
}
+
}
+
// convert NiceTree -> tangled.RepoTree_TreeEntry
treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files))
for i, file := range files {
···
Parent: parentPtr,
Dotdot: dotdotPtr,
Files: treeEntries,
+
Readme: &tangled.RepoTree_Readme{
+
Filename: readmeFileName,
+
Contents: readmeContents,
+
},
}
writeJson(w, response)
+19
lexicons/repo/tree.json
···
"type": "string",
"description": "Parent directory path"
},
+
"readme": {
+
"type": "ref",
+
"ref": "#readme",
+
"description": "Readme for this file tree"
+
},
"files": {
"type": "array",
"items": {
···
}
]
},
+
"readme": {
+
"type": "object",
+
"required": ["filename", "contents"],
+
"properties": {
+
"filename": {
+
"type": "string",
+
"description": "Name of the readme file"
+
},
+
"contents": {
+
"type": "string",
+
"description": "Contents of the readme file"
+
}
+
}
+
},
"treeEntry": {
"type": "object",
"required": ["name", "mode", "size", "is_file", "is_subtree"],
+7 -5
types/repo.go
···
}
type RepoTreeResponse struct {
-
Ref string `json:"ref,omitempty"`
-
Parent string `json:"parent,omitempty"`
-
Description string `json:"description,omitempty"`
-
DotDot string `json:"dotdot,omitempty"`
-
Files []NiceTree `json:"files,omitempty"`
+
Ref string `json:"ref,omitempty"`
+
Parent string `json:"parent,omitempty"`
+
Description string `json:"description,omitempty"`
+
DotDot string `json:"dotdot,omitempty"`
+
Files []NiceTree `json:"files,omitempty"`
+
ReadmeFileName string `json:"readme_filename,omitempty"`
+
Readme string `json:"readme_contents,omitempty"`
}
type TagReference struct {
-2
appview/pages/legal/privacy.md
···
-
# Privacy Policy
-
**Last updated:** September 26, 2025
This Privacy Policy describes how Tangled ("we," "us," or "our")
+2 -4
appview/pages/legal/terms.md
···
-
# Terms of Service
-
**Last updated:** September 26, 2025
Welcome to Tangled. These Terms of Service ("Terms") govern your access
···
## 3. Account Termination
> **Important Notice**
-
>
+
>
> **We reserve the right to terminate, suspend, or restrict access to
> your account at any time, for any reason, or for no reason at all, at
> our sole discretion.** This includes, but is not limited to,
> termination for violation of these Terms, inappropriate conduct, spam,
> abuse, or any other behavior we deem harmful to the Service or other
> users.
-
>
+
>
> Account termination may result in the loss of access to your
> repositories, data, and other content associated with your account. We
> are not obligated to provide advance notice of termination, though we
+224
appview/pages/templates/brand/brand.html
···
+
{{ define "title" }}brand{{ end }}
+
+
{{ define "content" }}
+
<div class="grid grid-cols-10">
+
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
+
<h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1>
+
<p class="text-gray-600 dark:text-gray-400 mb-1">
+
Assets and guidelines for using Tangled's logo and brand elements.
+
</p>
+
</header>
+
+
<main class="col-span-full md:col-span-10 bg-white dark:bg-gray-800 drop-shadow-sm rounded p-6 md:px-10">
+
<div class="space-y-16">
+
+
<!-- Introduction Section -->
+
<section>
+
<p class="text-gray-600 dark:text-gray-400 mb-2">
+
Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please
+
follow the below guidelines when using Dolly and the logotype.
+
</p>
+
<p class="text-gray-600 dark:text-gray-400 mb-2">
+
All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as".
+
</p>
+
</section>
+
+
<!-- Black Logotype Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded">
+
<img src="https://assets.tangled.network/tangled_logotype_black_on_trans.svg"
+
alt="Tangled logo - black version"
+
class="w-full max-w-sm mx-auto" />
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on light-colored backgrounds.</p>
+
<p class="text-gray-700 dark:text-gray-300">
+
This is the preferred version of the logotype, featuring dark text and elements, ideal for light
+
backgrounds and designs.
+
</p>
+
</div>
+
</section>
+
+
<!-- White Logotype Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="bg-black p-8 sm:p-16 rounded">
+
<img src="https://assets.tangled.network/tangled_logotype_white_on_trans.svg"
+
alt="Tangled logo - white version"
+
class="w-full max-w-sm mx-auto" />
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">For use on dark-colored backgrounds.</p>
+
<p class="text-gray-700 dark:text-gray-300">
+
This version features white text and elements, ideal for dark backgrounds
+
and inverted designs.
+
</p>
+
</div>
+
</section>
+
+
<!-- Mark Only Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="grid grid-cols-2 gap-2">
+
<!-- Black mark on light background -->
+
<div class="border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-100 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_black_on_trans.svg"
+
alt="Dolly face - black version"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- White mark on dark background -->
+
<div class="bg-black p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"
+
alt="Dolly face - white version"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
+
When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own.
+
</p>
+
<p class="text-gray-700 dark:text-gray-300 mb-4">
+
<strong class="font-semibold">Note</strong>: for situations where the background
+
is unknown, use the black version for ideal contrast in most environments.
+
</p>
+
</div>
+
</section>
+
+
<!-- Colored Backgrounds Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="grid grid-cols-2 gap-2">
+
<!-- Pastel Green background -->
+
<div class="bg-green-500 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"
+
alt="Tangled logo on pastel green background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Blue background -->
+
<div class="bg-blue-500 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"
+
alt="Tangled logo on pastel blue background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Yellow background -->
+
<div class="bg-yellow-500 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"
+
alt="Tangled logo on pastel yellow background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Red background -->
+
<div class="bg-red-500 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"
+
alt="Tangled logo on pastel red background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
+
White logo mark on colored backgrounds.
+
</p>
+
<p class="text-gray-700 dark:text-gray-300 mb-4">
+
The white logo mark provides contrast on colored backgrounds.
+
Perfect for more fun design contexts.
+
</p>
+
</div>
+
</section>
+
+
<!-- Black Logo on Pastel Backgrounds Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="grid grid-cols-2 gap-2">
+
<!-- Pastel Green background -->
+
<div class="bg-green-200 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_black_on_trans.svg"
+
alt="Tangled logo on pastel green background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Blue background -->
+
<div class="bg-blue-200 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_black_on_trans.svg"
+
alt="Tangled logo on pastel blue background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Yellow background -->
+
<div class="bg-yellow-200 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_black_on_trans.svg"
+
alt="Tangled logo on pastel yellow background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
<!-- Pastel Pink background -->
+
<div class="bg-pink-200 p-8 sm:p-12 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_face_only_black_on_trans.svg"
+
alt="Tangled logo on pastel pink background"
+
class="w-full max-w-16 mx-auto" />
+
</div>
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
+
Dark logo mark on lighter, pastel backgrounds.
+
</p>
+
<p class="text-gray-700 dark:text-gray-300 mb-4">
+
The dark logo mark works beautifully on pastel backgrounds,
+
providing crisp contrast.
+
</p>
+
</div>
+
</section>
+
+
<!-- Recoloring Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="bg-yellow-100 border border-yellow-200 p-8 sm:p-16 rounded">
+
<img src="https://assets.tangled.network/tangled_logotype_black_on_trans.svg"
+
alt="Recolored Tangled logotype in gray/sand color"
+
class="w-full max-w-sm mx-auto opacity-60 sepia contrast-75 saturate-50" />
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">
+
Custom coloring of the logotype is permitted.
+
</p>
+
<p class="text-gray-700 dark:text-gray-300 mb-4">
+
Recoloring the logotype is allowed as long as readability is maintained.
+
</p>
+
<p class="text-gray-700 dark:text-gray-300 text-sm">
+
<strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background.
+
</p>
+
</div>
+
</section>
+
+
<!-- Silhouette Section -->
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
+
<div class="order-2 lg:order-1">
+
<div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded">
+
<img src="https://assets.tangled.network/tangled_dolly_silhouette.svg"
+
alt="Dolly silhouette"
+
class="w-full max-w-32 mx-auto" />
+
</div>
+
</div>
+
<div class="order-1 lg:order-2">
+
<h2 class="text-xl font-semibold dark:text-white mb-3">Dolly silhouette</h2>
+
<p class="text-gray-600 dark:text-gray-400 mb-4">A minimalist version of Dolly.</p>
+
<p class="text-gray-700 dark:text-gray-300">
+
The silhouette can be used where a subtle brand presence is needed,
+
or as a background element. Works on any background color with proper contrast.
+
For example, we use this as the site's favicon.
+
</p>
+
</div>
+
</section>
+
+
</div>
+
</main>
+
</div>
+
{{ end }}
+13 -6
appview/pages/templates/legal/privacy.html
···
{{ define "title" }}privacy policy{{ end }}
{{ define "content" }}
-
<div class="max-w-4xl mx-auto px-4 py-8">
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8">
-
<div class="prose prose-gray dark:prose-invert max-w-none">
-
{{ .Content }}
-
</div>
+
<div class="grid grid-cols-10">
+
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
+
<h1 class="text-2xl font-bold dark:text-white mb-1">Privacy Policy</h1>
+
<p class="text-gray-600 dark:text-gray-400 mb-1">
+
Learn how we collect, use, and protect your personal information.
+
</p>
+
</header>
+
+
<main class="col-span-full md:col-span-10 bg-white dark:bg-gray-800 drop-shadow-sm rounded p-6 md:px-10">
+
<div class="prose prose-gray dark:prose-invert max-w-none">
+
{{ .Content }}
</div>
+
</main>
</div>
-
{{ end }}
+
{{ end }}
+13 -6
appview/pages/templates/legal/terms.html
···
{{ define "title" }}terms of service{{ end }}
{{ define "content" }}
-
<div class="max-w-4xl mx-auto px-4 py-8">
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-8">
-
<div class="prose prose-gray dark:prose-invert max-w-none">
-
{{ .Content }}
-
</div>
+
<div class="grid grid-cols-10">
+
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
+
<h1 class="text-2xl font-bold dark:text-white mb-1">Terms of Service</h1>
+
<p class="text-gray-600 dark:text-gray-400 mb-1">
+
A few things you should know.
+
</p>
+
</header>
+
+
<main class="col-span-full md:col-span-10 bg-white dark:bg-gray-800 drop-shadow-sm rounded p-6 md:px-10">
+
<div class="prose prose-gray dark:prose-invert max-w-none">
+
{{ .Content }}
</div>
+
</main>
</div>
-
{{ end }}
+
{{ end }}
+1 -1
appview/pages/templates/user/fragments/followCard.html
···
{{ define "user/fragments/followCard" }}
{{ $userIdent := resolve .UserDid }}
-
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
+
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 rounded-sm">
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
<div class="flex-shrink-0 max-h-full w-24 h-24">
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" />
+4 -2
appview/config/config.go
···
}
type Cloudflare struct {
-
ApiToken string `env:"API_TOKEN"`
-
ZoneId string `env:"ZONE_ID"`
+
ApiToken string `env:"API_TOKEN"`
+
ZoneId string `env:"ZONE_ID"`
+
TurnstileSiteKey string `env:"TURNSTILE_SITE_KEY"`
+
TurnstileSecretKey string `env:"TURNSTILE_SECRET_KEY"`
}
func (cfg RedisConfig) ToURL() string {
+13 -9
appview/db/email.go
···
return did, nil
}
-
func GetEmailToDid(e Execer, ems []string, isVerifiedFilter bool) (map[string]string, error) {
-
if len(ems) == 0 {
+
func GetEmailToDid(e Execer, emails []string, isVerifiedFilter bool) (map[string]string, error) {
+
if len(emails) == 0 {
return make(map[string]string), nil
}
···
verifiedFilter = 1
}
+
assoc := make(map[string]string)
+
// Create placeholders for the IN clause
-
placeholders := make([]string, len(ems))
-
args := make([]any, len(ems)+1)
+
placeholders := make([]string, 0, len(emails))
+
args := make([]any, 1, len(emails)+1)
args[0] = verifiedFilter
-
for i, em := range ems {
-
placeholders[i] = "?"
-
args[i+1] = em
+
for _, email := range emails {
+
if strings.HasPrefix(email, "did:") {
+
assoc[email] = email
+
continue
+
}
+
placeholders = append(placeholders, "?")
+
args = append(args, email)
}
query := `
···
}
defer rows.Close()
-
assoc := make(map[string]string)
-
for rows.Next() {
var email, did string
if err := rows.Scan(&email, &did); err != nil {
+7
appview/pages/templates/repo/fork.html
···
</div>
<div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded">
<form hx-post="/{{ .RepoInfo.FullName }}/fork" class="space-y-12" hx-swap="none" hx-indicator="#spinner">
+
+
<fieldset class="space-y-3">
+
<legend for="repo_name" class="dark:text-white">Repository name</legend>
+
<input type="text" id="repo_name" name="repo_name" value="{{ .RepoInfo.Name }}"
+
class="w-full p-2 border rounded bg-gray-100 dark:bg-gray-700 dark:text-white dark:border-gray-600" />
+
</fieldset>
+
<fieldset class="space-y-3">
<legend class="dark:text-white">Select a knot to fork into</legend>
<div class="space-y-2">
+11 -7
appview/repo/repo.go
···
// choose a name for a fork
-
forkName := f.Name
+
forkName := r.FormValue("repo_name")
+
if forkName == "" {
+
rp.pages.Notice(w, "repo", "Repository name cannot be empty.")
+
return
+
}
+
// this check is *only* to see if the forked repo name already exists
// in the user's account.
existingRepo, err := db.GetRepo(
rp.db,
db.FilterEq("did", user.Did),
-
db.FilterEq("name", f.Name),
+
db.FilterEq("name", forkName),
if err != nil {
-
if errors.Is(err, sql.ErrNoRows) {
-
// no existing repo with this name found, we can use the name as is
-
} else {
+
if !errors.Is(err, sql.ErrNoRows) {
log.Println("error fetching existing repo from db", "err", err)
rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
} else if existingRepo != nil {
-
// repo with this name already exists, append random string
-
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
+
// repo with this name already exists
+
rp.pages.Notice(w, "repo", "A repository with this name already exists.")
+
return
l = l.With("forkName", forkName)
+140
appview/db/db.go
···
return err
})
+
// add generated at_uri column to pulls table
+
//
+
// this requires a full table recreation because stored columns
+
// cannot be added via alter
+
//
+
// disable foreign-keys for the next migration
+
conn.ExecContext(ctx, "pragma foreign_keys = off;")
+
runMigration(conn, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
create table if not exists pulls_new (
+
-- identifiers
+
id integer primary key autoincrement,
+
pull_id integer not null,
+
at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored,
+
+
-- at identifiers
+
repo_at text not null,
+
owner_did text not null,
+
rkey text not null,
+
+
-- content
+
title text not null,
+
body text not null,
+
target_branch text not null,
+
state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
+
+
-- source info
+
source_branch text,
+
source_repo_at text,
+
+
-- stacking
+
stack_id text,
+
change_id text,
+
parent_change_id text,
+
+
-- meta
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
unique(repo_at, pull_id),
+
unique(at_uri),
+
foreign key (repo_at) references repos(at_uri) on delete cascade
+
);
+
`)
+
if err != nil {
+
return err
+
}
+
+
// transfer data
+
_, err = tx.Exec(`
+
insert into pulls_new (
+
id, pull_id, repo_at, owner_did, rkey,
+
title, body, target_branch, state,
+
source_branch, source_repo_at,
+
stack_id, change_id, parent_change_id,
+
created
+
)
+
select
+
id, pull_id, repo_at, owner_did, rkey,
+
title, body, target_branch, state,
+
source_branch, source_repo_at,
+
stack_id, change_id, parent_change_id,
+
created
+
from pulls;
+
`)
+
if err != nil {
+
return err
+
}
+
+
// drop old table
+
_, err = tx.Exec(`drop table pulls`)
+
if err != nil {
+
return err
+
}
+
+
// rename new table
+
_, err = tx.Exec(`alter table pulls_new rename to pulls`)
+
return err
+
})
+
conn.ExecContext(ctx, "pragma foreign_keys = on;")
+
+
// remove repo_at and pull_id from pull_submissions and replace with pull_at
+
//
+
// this requires a full table recreation because stored columns
+
// cannot be added via alter
+
//
+
// disable foreign-keys for the next migration
+
conn.ExecContext(ctx, "pragma foreign_keys = off;")
+
runMigration(conn, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
+
_, err := tx.Exec(`
+
create table if not exists pull_submissions_new (
+
-- identifiers
+
id integer primary key autoincrement,
+
pull_at text not null,
+
+
-- content, these are immutable, and require a resubmission to update
+
round_number integer not null default 0,
+
patch text,
+
source_rev text,
+
+
-- meta
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
+
-- constraints
+
unique(pull_at, round_number),
+
foreign key (pull_at) references pulls(at_uri) on delete cascade
+
);
+
`)
+
if err != nil {
+
return err
+
}
+
+
// transfer data, constructing pull_at from pulls table
+
_, err = tx.Exec(`
+
insert into pull_submissions_new (id, pull_at, round_number, patch, created)
+
select
+
ps.id,
+
'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
+
ps.round_number,
+
ps.patch,
+
ps.created
+
from pull_submissions ps
+
join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id;
+
`)
+
if err != nil {
+
return err
+
}
+
+
// drop old table
+
_, err = tx.Exec(`drop table pull_submissions`)
+
if err != nil {
+
return err
+
}
+
+
// rename new table
+
_, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`)
+
return err
+
})
+
conn.ExecContext(ctx, "pragma foreign_keys = on;")
+
return &DB{db}, nil
}
+5 -1
appview/issues/issues.go
···
return
}
-
labelDefs, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
labelDefs, err := db.GetLabelDefinitions(
+
rp.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoIssueNSID),
+
)
if err != nil {
log.Println("failed to fetch labels", err)
rp.pages.Error503(w)
+1 -1
appview/pages/templates/repo/fragments/labelPanel.html
···
{{ define "repo/fragments/labelPanel" }}
-
<div id="label-panel" class="flex flex-col gap-6">
+
<div id="label-panel" class="flex flex-col gap-6 px-6 md:px-0">
{{ template "basicLabels" . }}
{{ template "kvLabels" . }}
</div>
+26
appview/pages/templates/repo/fragments/participants.html
···
+
{{ define "repo/fragments/participants" }}
+
{{ $all := . }}
+
{{ $ps := take $all 5 }}
+
<div class="px-6 md:px-0">
+
<div class="py-1 flex items-center text-sm">
+
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
+
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
+
</div>
+
<div class="flex items-center -space-x-3 mt-2">
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
+
{{ range $i, $p := $ps }}
+
<img
+
src="{{ tinyAvatar . }}"
+
alt=""
+
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
+
/>
+
{{ end }}
+
+
{{ if gt (len $all) 5 }}
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
+
+{{ sub (len $all) 5 }}
+
</span>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+1 -27
appview/pages/templates/repo/issues/issue.html
···
"Defs" $.LabelDefs
"Subject" $.Issue.AtUri
"State" $.Issue.Labels) }}
-
{{ template "issueParticipants" . }}
+
{{ template "repo/fragments/participants" $.Issue.Participants }}
</div>
</div>
{{ end }}
···
</div>
{{ end }}
-
{{ define "issueParticipants" }}
-
{{ $all := .Issue.Participants }}
-
{{ $ps := take $all 5 }}
-
<div>
-
<div class="py-1 flex items-center text-sm">
-
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
-
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
-
</div>
-
<div class="flex items-center -space-x-3 mt-2">
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
-
{{ range $i, $p := $ps }}
-
<img
-
src="{{ tinyAvatar . }}"
-
alt=""
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
-
/>
-
{{ end }}
-
-
{{ if gt (len $all) 5 }}
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
-
+{{ sub (len $all) 5 }}
-
</span>
-
{{ end }}
-
</div>
-
</div>
-
{{ end }}
{{ define "repoAfter" }}
<div class="flex flex-col gap-4 mt-4">
+30 -12
appview/pages/templates/repo/pulls/pull.html
···
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
{{ end }}
+
{{ define "repoContentLayout" }}
+
<div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full">
+
<div class="col-span-1 md:col-span-8">
+
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white">
+
{{ block "repoContent" . }}{{ end }}
+
</section>
+
{{ block "repoAfter" . }}{{ end }}
+
</div>
+
<div class="col-span-1 md:col-span-2 flex flex-col gap-6">
+
{{ template "repo/fragments/labelPanel"
+
(dict "RepoInfo" $.RepoInfo
+
"Defs" $.LabelDefs
+
"Subject" $.Pull.PullAt
+
"State" $.Pull.Labels) }}
+
{{ template "repo/fragments/participants" $.Pull.Participants }}
+
</div>
+
</div>
+
{{ end }}
{{ define "repoContent" }}
{{ template "repo/pulls/fragments/pullHeader" . }}
···
{{ with $item }}
<details {{ if eq $idx $lastIdx }}open{{ end }}>
<summary id="round-#{{ .RoundNumber }}" class="list-none cursor-pointer">
-
<div class="flex flex-wrap gap-2 items-center">
+
<div class="flex flex-wrap gap-2 items-stretch">
<!-- round number -->
<div class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-3 py-2 dark:text-white">
<span class="flex items-center">{{ i "hash" "w-4 h-4" }}{{ .RoundNumber }}</span>
</div>
<!-- round summary -->
-
<div class="rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
+
<div class="flex-1 rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400">
<span class="gap-1 flex items-center">
{{ $owner := resolve $.Pull.OwnerDid }}
{{ $re := "re" }}
···
<span class="hidden md:inline">diff</span>
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
</a>
-
{{ if not (eq .RoundNumber 0) }}
-
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
-
hx-boost="true"
-
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
-
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
-
<span class="hidden md:inline">interdiff</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</a>
-
<span id="interdiff-error-{{.RoundNumber}}"></span>
+
{{ if ne $idx 0 }}
+
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
+
hx-boost="true"
+
href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff">
+
{{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }}
+
<span class="hidden md:inline">interdiff</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</a>
{{ end }}
+
<span id="interdiff-error-{{.RoundNumber}}"></span>
</div>
</summary>
···
<div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative">
{{ range $cidx, $c := .Comments }}
-
<div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit">
+
<div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full">
{{ if gt $cidx 0 }}
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
{{ end }}
+7
appview/pages/templates/repo/pulls/pulls.html
···
<span class="before:content-['ยท']"></span>
{{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }}
{{ end }}
+
+
{{ $state := .Labels }}
+
{{ range $k, $d := $.LabelDefs }}
+
{{ range $v, $s := $state.GetValSet $d.AtUri.String }}
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
+
{{ end }}
+
{{ end }}
</div>
</div>
{{ if .StackId }}
+35
appview/pulls/pulls.go
···
userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.PullAt())
}
+
labelDefs, err := db.GetLabelDefinitions(
+
s.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoPullNSID),
+
)
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
s.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*models.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
LoggedInUser: user,
RepoInfo: repoInfo,
···
OrderedReactionKinds: models.OrderedReactionKinds,
Reactions: reactionCountMap,
UserReacted: userReactions,
+
+
LabelDefs: defs,
})
}
···
m[p.Sha] = p
}
+
labelDefs, err := db.GetLabelDefinitions(
+
s.db,
+
db.FilterIn("at_uri", f.Repo.Labels),
+
db.FilterContains("scope", tangled.RepoPullNSID),
+
)
+
if err != nil {
+
log.Println("failed to fetch labels", err)
+
s.pages.Error503(w)
+
return
+
}
+
+
defs := make(map[string]*models.LabelDefinition)
+
for _, l := range labelDefs {
+
defs[l.AtUri().String()] = &l
+
}
+
s.pages.RepoPulls(w, pages.RepoPullsParams{
LoggedInUser: s.oauth.GetUser(r),
RepoInfo: f.RepoInfo(user),
Pulls: pulls,
+
LabelDefs: defs,
FilteringBy: state,
Stacks: stacks,
Pipelines: m,
+8 -48
appview/notify/db/db.go
···
func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) {
var err error
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(star.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
if err != nil {
log.Printf("NewStar: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewStar: no repo found for %s", star.RepoAt)
-
return
-
}
-
repo := repos[0]
// don't notify yourself
if repo.Did == star.StarredByDid {
···
}
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssue: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssue: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
if repo.Did == issue.Did {
return
···
}
issue := issues[0]
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssueComment: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssueComment: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
recipients := make(map[string]bool)
···
}
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPull: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPull: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
if repo.Did == pull.OwnerDid {
return
···
}
pull := pulls[0]
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", comment.RepoAt))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
if err != nil {
log.Printf("NewPullComment: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullComment: no repo found for %s", comment.RepoAt)
-
return
-
}
-
repo := repos[0]
recipients := make(map[string]bool)
···
func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
if err != nil {
log.Printf("NewIssueClosed: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewIssueClosed: no repo found for %s", issue.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == issue.Did {
···
func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPullMerged: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullMerged: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == pull.OwnerDid {
···
func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
// Get repo details
-
repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt)))
+
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
if err != nil {
log.Printf("NewPullClosed: failed to get repos: %v", err)
return
}
-
if len(repos) == 0 {
-
log.Printf("NewPullClosed: no repo found for %s", pull.RepoAt)
-
return
-
}
-
repo := repos[0]
// Don't notify yourself
if repo.Did == pull.OwnerDid {
+29 -1
appview/models/notifications.go
···
package models
-
import "time"
+
import (
+
"time"
+
)
type NotificationType string
···
PullId *int64
}
+
// lucide icon that represents this notification
+
func (n *Notification) Icon() string {
+
switch n.Type {
+
case NotificationTypeRepoStarred:
+
return "star"
+
case NotificationTypeIssueCreated:
+
return "circle-dot"
+
case NotificationTypeIssueCommented:
+
return "message-square"
+
case NotificationTypeIssueClosed:
+
return "ban"
+
case NotificationTypePullCreated:
+
return "git-pull-request-create"
+
case NotificationTypePullCommented:
+
return "message-square"
+
case NotificationTypePullMerged:
+
return "git-merge"
+
case NotificationTypePullClosed:
+
return "git-pull-request-closed"
+
case NotificationTypeFollowed:
+
return "user-plus"
+
default:
+
return ""
+
}
+
}
+
type NotificationWithEntity struct {
*Notification
Repo *Repo
+1 -1
appview/pagination/page.go
···
func FirstPage() Page {
return Page{
Offset: 0,
-
Limit: 10,
+
Limit: 30,
}
}
+1 -1
nix/pkgs/knot-unwrapped.nix
···
sqlite-lib,
src,
}: let
-
version = "1.9.0-alpha";
+
version = "1.9.1-alpha";
in
buildGoApplication {
pname = "knot";
+1 -1
appview/pages/templates/repo/fragments/cloneDropdown.html
···
{{ define "repo/fragments/cloneDropdown" }}
{{ $knot := .RepoInfo.Knot }}
{{ if eq $knot "knot1.tangled.sh" }}
-
{{ $knot = "tangled.sh" }}
+
{{ $knot = "tangled.org" }}
{{ end }}
<details id="clone-dropdown" class="relative inline-block text-left group">
+1 -1
docs/spindle/pipeline.md
···
- `manual`: The workflow can be triggered manually.
- `branch`: This is a **required** field that defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event.
-
For example, if you'd like define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
+
For example, if you'd like to define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with:
```yaml
when:
+1 -1
knotserver/config/config.go
···
Repo Repo `env:",prefix=KNOT_REPO_"`
Server Server `env:",prefix=KNOT_SERVER_"`
Git Git `env:",prefix=KNOT_GIT_"`
-
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.sh"`
+
AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.org"`
}
func Load(ctx context.Context) (*Config, error) {
+3
appview/pages/templates/layouts/base.html
···
<link rel="preconnect" href="https://avatar.tangled.sh" />
<link rel="preconnect" href="https://camo.tangled.sh" />
+
<!-- pwa manifest -->
+
<link rel="manifest" href="/pwa-manifest.json" />
+
<!-- preload main font -->
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
+1
appview/pages/templates/user/completeSignup.html
···
content="complete your signup for tangled"
/>
<script src="/static/htmx.min.js"></script>
+
<link rel="manifest" href="/pwa-manifest.json" />
<link
rel="stylesheet"
href="/static/tw.css?{{ cssContentHash }}"
+1
appview/pages/templates/user/signup.html
···
<meta property="og:url" content="https://tangled.org/signup" />
<meta property="og:description" content="sign up for tangled" />
<script src="/static/htmx.min.js"></script>
+
<link rel="manifest" href="/pwa-manifest.json" />
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
<title>sign up &middot; tangled</title>
+34
appview/db/language.go
···
package db
import (
+
"database/sql"
"fmt"
"strings"
+
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
)
···
return nil
}
+
+
func DeleteRepoLanguages(e Execer, filters ...filter) 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(`delete from repo_languages %s`, whereClause)
+
+
_, err := e.Exec(query, args...)
+
return err
+
}
+
+
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
+
err := DeleteRepoLanguages(
+
tx,
+
FilterEq("repo_at", repoAt),
+
FilterEq("ref", ref),
+
)
+
if err != nil {
+
return fmt.Errorf("failed to delete existing languages: %w", err)
+
}
+
+
return InsertRepoLanguages(tx, langs)
+
}
+14 -1
appview/state/knotstream.go
···
})
}
-
return db.InsertRepoLanguages(d, langs)
+
tx, err := d.Begin()
+
if err != nil {
+
return err
+
}
+
defer tx.Rollback()
+
+
// update appview's cache
+
err = db.UpdateRepoLanguages(tx, repo.RepoAt(), ref.Short(), langs)
+
if err != nil {
+
fmt.Printf("failed; %s\n", err)
+
// non-fatal
+
}
+
+
return tx.Commit()
}
func ingestPipeline(d *db.DB, source ec.Source, msg ec.Message) error {