chore: added clone button
eec14b33
7 file(s) · +173 −36
| 8 | 8 | KEPLER_SITE_NAME=kepler |
|
| 9 | 9 | # Public base URL, used for Open Graph / social meta tags |
|
| 10 | 10 | KEPLER_BASE_URL=http://localhost:4747 |
|
| 11 | + | # HTTPS entry in the repo-home Clone menu (URL = <base>/<repo>.git). |
|
| 12 | + | # Point at the host that actually serves git (e.g. softserve). Empty = omit. |
|
| 13 | + | KEPLER_CLONE_BASE_URL= |
|
| 14 | + | # SSH (scp-style) entry in the Clone menu (URL = <user@host>:<repo>.git). |
|
| 15 | + | # Include the user, e.g. git@git.example.com. Empty = omit. |
|
| 16 | + | # If both clone vars are empty the Clone button is hidden. |
|
| 17 | + | KEPLER_CLONE_SSH_HOST= |
| 33 | 33 | | `KEPLER_REPO_ROOT` | `./repos` | dir of bare repos (`*.git/`) or normal repos | |
|
| 34 | 34 | | `KEPLER_SITE_NAME` | `kepler` | shown in header + feed | |
|
| 35 | 35 | | `KEPLER_BASE_URL` | `http://localhost:4747` | public URL for Open Graph / social meta tags | |
|
| 36 | + | | `KEPLER_CLONE_BASE_URL` | _(empty)_ | HTTPS entry in the repo-home Clone menu; URL = `<base>/<repo>.git`. Point at the host that actually serves git (e.g. softserve), not kepler | |
|
| 37 | + | | `KEPLER_CLONE_SSH_HOST` | _(empty)_ | SSH (scp-style) entry in the Clone menu; URL = `<user@host>:<repo>.git`, e.g. `git@git.example.com`. Include the user | |
|
| 38 | + | ||
| 39 | + | The repo-home **Clone** dropdown shows whichever of the two are set; if both |
|
| 40 | + | are empty the button is hidden. |
|
| 36 | 41 | ||
| 37 | 42 | ## Repo discovery |
|
| 38 | 43 |
| 11 | 11 | var appFS embed.FS |
|
| 12 | 12 | ||
| 13 | 13 | type App struct { |
|
| 14 | - | Log *slog.Logger |
|
| 15 | - | Templates map[string]*template.Template |
|
| 16 | - | RepoRoot string |
|
| 17 | - | SiteName string |
|
| 18 | - | BaseURL string |
|
| 14 | + | Log *slog.Logger |
|
| 15 | + | Templates map[string]*template.Template |
|
| 16 | + | RepoRoot string |
|
| 17 | + | SiteName string |
|
| 18 | + | BaseURL string |
|
| 19 | + | CloneBaseURL string |
|
| 20 | + | CloneSSHHost string |
|
| 19 | 21 | } |
|
| 20 | 22 | ||
| 21 | 23 | type pageBase struct { |
|
| 31 | 33 | ||
| 32 | 34 | type repoPageData struct { |
|
| 33 | 35 | pageBase |
|
| 34 | - | Repo RepoSummary |
|
| 35 | - | Ref string |
|
| 36 | - | ReadmeHTML template.HTML |
|
| 37 | - | HasReadme bool |
|
| 38 | - | Branches []RefInfo |
|
| 39 | - | Tags []RefInfo |
|
| 40 | - | Commits []CommitInfo |
|
| 41 | - | DefaultRef string |
|
| 42 | - | LatestCommit CommitInfo |
|
| 43 | - | HasLatest bool |
|
| 44 | - | Entries []TreeEntry |
|
| 36 | + | Repo RepoSummary |
|
| 37 | + | Ref string |
|
| 38 | + | ReadmeHTML template.HTML |
|
| 39 | + | HasReadme bool |
|
| 40 | + | Branches []RefInfo |
|
| 41 | + | Tags []RefInfo |
|
| 42 | + | Commits []CommitInfo |
|
| 43 | + | DefaultRef string |
|
| 44 | + | LatestCommit CommitInfo |
|
| 45 | + | HasLatest bool |
|
| 46 | + | Entries []TreeEntry |
|
| 47 | + | CloneHTTPSURL string |
|
| 48 | + | CloneSSHURL string |
|
| 45 | 49 | } |
|
| 46 | 50 | ||
| 47 | 51 | type treePageData struct { |
|
| 56 | 60 | ||
| 57 | 61 | type blobPageData struct { |
|
| 58 | 62 | pageBase |
|
| 59 | - | Repo RepoSummary |
|
| 60 | - | Ref string |
|
| 61 | - | DefaultRef string |
|
| 62 | - | Path string |
|
| 63 | - | Breadcrumbs []Breadcrumb |
|
| 64 | - | Binary bool |
|
| 65 | - | Size int64 |
|
| 63 | + | Repo RepoSummary |
|
| 64 | + | Ref string |
|
| 65 | + | DefaultRef string |
|
| 66 | + | Path string |
|
| 67 | + | Breadcrumbs []Breadcrumb |
|
| 68 | + | Binary bool |
|
| 69 | + | Size int64 |
|
| 66 | 70 | HighlightedHTML template.HTML |
|
| 67 | 71 | } |
|
| 68 | 72 | ||
| 53 | 53 | Entries: entries, |
|
| 54 | 54 | } |
|
| 55 | 55 | ||
| 56 | + | if a.CloneBaseURL != "" { |
|
| 57 | + | data.CloneHTTPSURL = a.CloneBaseURL + "/" + summary.Name + ".git" |
|
| 58 | + | } |
|
| 59 | + | if a.CloneSSHHost != "" { |
|
| 60 | + | data.CloneSSHURL = a.CloneSSHHost + ":" + summary.Name + ".git" |
|
| 61 | + | } |
|
| 62 | + | ||
| 56 | 63 | if c, err := resolveRef(repo, summary.DefaultRef); err == nil { |
|
| 57 | 64 | if tree, err := c.Tree(); err == nil { |
|
| 58 | 65 | if src, ok := findReadme(tree); ok { |
| 17 | 17 | root := config.Getenv("KEPLER_REPO_ROOT", "./repos") |
|
| 18 | 18 | siteName := config.Getenv("KEPLER_SITE_NAME", "kepler") |
|
| 19 | 19 | baseURL := config.Getenv("KEPLER_BASE_URL", "http://localhost:4747") |
|
| 20 | + | cloneBaseURL := config.Getenv("KEPLER_CLONE_BASE_URL", "") |
|
| 21 | + | cloneSSHHost := config.Getenv("KEPLER_CLONE_SSH_HOST", "") |
|
| 20 | 22 | ||
| 21 | 23 | tmpl, err := buildTemplates() |
|
| 22 | 24 | if err != nil { |
|
| 24 | 26 | } |
|
| 25 | 27 | ||
| 26 | 28 | app := &App{ |
|
| 27 | - | Log: logger, |
|
| 28 | - | Templates: tmpl, |
|
| 29 | - | RepoRoot: root, |
|
| 30 | - | SiteName: siteName, |
|
| 31 | - | BaseURL: strings.TrimRight(baseURL, "/"), |
|
| 29 | + | Log: logger, |
|
| 30 | + | Templates: tmpl, |
|
| 31 | + | RepoRoot: root, |
|
| 32 | + | SiteName: siteName, |
|
| 33 | + | BaseURL: strings.TrimRight(baseURL, "/"), |
|
| 34 | + | CloneBaseURL: strings.TrimRight(cloneBaseURL, "/"), |
|
| 35 | + | CloneSSHHost: strings.TrimSpace(cloneSSHHost), |
|
| 32 | 36 | } |
|
| 33 | 37 | ||
| 34 | 38 | addr := config.Getenv("HOST", "127.0.0.1") + ":" + config.Getenv("PORT", "4747") |
|
| 138 | 138 | .repo-nav .tab:first-of-type { margin-left: 0; } |
|
| 139 | 139 | .tab { padding: 0.4rem 0.6rem; } |
|
| 140 | 140 | ||
| 141 | + | .repo-head { flex-direction: column; gap: 0.75rem; align-items: stretch; } |
|
| 142 | + | .clone-toggle { width: 100%; } |
|
| 143 | + | .clone-menu { left: 0; right: 0; min-width: 0; } |
|
| 141 | 144 | .last-commit { flex-wrap: wrap; gap: 0.25rem 0.75rem; padding: 0.5rem 0.75rem; } |
|
| 142 | 145 | .last-commit .subject { |
|
| 143 | 146 | flex: 1 1 100%; |
|
| 170 | 173 | .breadcrumbs { font-size: 13px; margin-bottom: 1rem; color: var(--fg-dim); } |
|
| 171 | 174 | .breadcrumbs a { color: var(--fg); } |
|
| 172 | 175 | ||
| 173 | - | /* --- Last commit bar --- */ |
|
| 176 | + | /* --- Repo head (last commit + clone) --- */ |
|
| 177 | + | ||
| 178 | + | .repo-head { |
|
| 179 | + | display: flex; |
|
| 180 | + | align-items: stretch; |
|
| 181 | + | gap: 1rem; |
|
| 182 | + | margin-bottom: 1rem; |
|
| 183 | + | } |
|
| 174 | 184 | ||
| 175 | 185 | .last-commit { |
|
| 176 | 186 | display: flex; |
|
| 180 | 190 | padding: 0.4rem 0.75rem; |
|
| 181 | 191 | font-size: 13px; |
|
| 182 | 192 | overflow: hidden; |
|
| 183 | - | margin-bottom: 1rem; |
|
| 193 | + | flex: 1; |
|
| 194 | + | min-width: 0; |
|
| 184 | 195 | } |
|
| 185 | 196 | .last-commit .subject { flex: 1; } |
|
| 197 | + | ||
| 198 | + | .clone-dropdown { position: relative; } |
|
| 199 | + | .clone-toggle { |
|
| 200 | + | height: 100%; |
|
| 201 | + | background: var(--bg-alt); |
|
| 202 | + | border: 1px solid var(--border); |
|
| 203 | + | color: var(--fg); |
|
| 204 | + | padding: 0.4rem 0.9rem; |
|
| 205 | + | font: inherit; |
|
| 206 | + | font-size: 13px; |
|
| 207 | + | cursor: pointer; |
|
| 208 | + | white-space: nowrap; |
|
| 209 | + | } |
|
| 210 | + | .clone-toggle::after { content: " \25be"; } |
|
| 211 | + | .clone-toggle:hover { background: var(--border); } |
|
| 212 | + | ||
| 213 | + | .clone-menu { |
|
| 214 | + | position: absolute; |
|
| 215 | + | top: calc(100% + 4px); |
|
| 216 | + | right: 0; |
|
| 217 | + | z-index: 10; |
|
| 218 | + | min-width: 18rem; |
|
| 219 | + | background: var(--bg); |
|
| 220 | + | border: 1px solid var(--border); |
|
| 221 | + | padding: 0.5rem; |
|
| 222 | + | display: flex; |
|
| 223 | + | flex-direction: column; |
|
| 224 | + | gap: 0.5rem; |
|
| 225 | + | } |
|
| 226 | + | .clone-menu[hidden] { display: none; } |
|
| 227 | + | .clone-option { display: flex; flex-direction: column; gap: 0.25rem; } |
|
| 228 | + | .clone-label { font-size: 11px; color: var(--fg-dim); text-transform: uppercase; } |
|
| 229 | + | .clone-option .copy-btn { align-self: flex-end; } |
|
| 230 | + | .clone-url { |
|
| 231 | + | display: block; |
|
| 232 | + | color: var(--fg); |
|
| 233 | + | font-family: var(--mono); |
|
| 234 | + | font-size: 12px; |
|
| 235 | + | background: var(--bg-alt); |
|
| 236 | + | border: 1px solid var(--border); |
|
| 237 | + | padding: 0.25rem 0.4rem; |
|
| 238 | + | white-space: nowrap; |
|
| 239 | + | overflow: auto hidden; |
|
| 240 | + | } |
|
| 186 | 241 | ||
| 187 | 242 | /* --- Repo home grid --- */ |
|
| 188 | 243 | ||
| 6 | 6 | <a href="/{{.Repo.Name}}/refs" class="tab">refs</a> |
|
| 7 | 7 | {{end}} |
|
| 8 | 8 | {{define "content"}} |
|
| 9 | - | {{if .HasLatest}} |
|
| 10 | - | <div class="last-commit"> |
|
| 11 | - | <span class="hash"><a href="/{{.Repo.Name}}/commit/{{.LatestCommit.SHA}}">{{.LatestCommit.ShortSHA}}</a></span> |
|
| 12 | - | <span class="subject">{{.LatestCommit.Subject}}</span> |
|
| 13 | - | <span class="author">{{.LatestCommit.Author}}</span> |
|
| 14 | - | <span class="date">{{timeAgo .LatestCommit.When}}</span> |
|
| 9 | + | {{if or .HasLatest .CloneHTTPSURL .CloneSSHURL}} |
|
| 10 | + | <div class="repo-head"> |
|
| 11 | + | {{if .HasLatest}} |
|
| 12 | + | <div class="last-commit"> |
|
| 13 | + | <span class="hash"><a href="/{{.Repo.Name}}/commit/{{.LatestCommit.SHA}}">{{.LatestCommit.ShortSHA}}</a></span> |
|
| 14 | + | <span class="subject">{{.LatestCommit.Subject}}</span> |
|
| 15 | + | <span class="author">{{.LatestCommit.Author}}</span> |
|
| 16 | + | <span class="date">{{timeAgo .LatestCommit.When}}</span> |
|
| 17 | + | </div> |
|
| 18 | + | {{end}} |
|
| 19 | + | {{if or .CloneHTTPSURL .CloneSSHURL}} |
|
| 20 | + | <div class="clone-dropdown"> |
|
| 21 | + | <button type="button" class="clone-toggle" aria-expanded="false">Clone</button> |
|
| 22 | + | <div class="clone-menu" hidden> |
|
| 23 | + | {{if .CloneHTTPSURL}} |
|
| 24 | + | <div class="clone-option"> |
|
| 25 | + | <span class="clone-label">HTTPS</span> |
|
| 26 | + | <code class="clone-url">{{.CloneHTTPSURL}}</code> |
|
| 27 | + | <button type="button" class="copy-btn clone-copy" data-clone="{{.CloneHTTPSURL}}">copy</button> |
|
| 28 | + | </div> |
|
| 29 | + | {{end}} |
|
| 30 | + | {{if .CloneSSHURL}} |
|
| 31 | + | <div class="clone-option"> |
|
| 32 | + | <span class="clone-label">SSH</span> |
|
| 33 | + | <code class="clone-url">{{.CloneSSHURL}}</code> |
|
| 34 | + | <button type="button" class="copy-btn clone-copy" data-clone="{{.CloneSSHURL}}">copy</button> |
|
| 35 | + | </div> |
|
| 36 | + | {{end}} |
|
| 37 | + | </div> |
|
| 38 | + | </div> |
|
| 39 | + | {{end}} |
|
| 15 | 40 | </div> |
|
| 16 | 41 | {{end}} |
|
| 17 | 42 | ||
| 43 | 68 | {{end}} |
|
| 44 | 69 | </section> |
|
| 45 | 70 | </div> |
|
| 71 | + | <script> |
|
| 72 | + | document.querySelectorAll(".clone-dropdown").forEach(function (dd) { |
|
| 73 | + | var toggle = dd.querySelector(".clone-toggle"); |
|
| 74 | + | var menu = dd.querySelector(".clone-menu"); |
|
| 75 | + | toggle.addEventListener("click", function (e) { |
|
| 76 | + | e.stopPropagation(); |
|
| 77 | + | var open = menu.hidden; |
|
| 78 | + | menu.hidden = !open; |
|
| 79 | + | toggle.setAttribute("aria-expanded", String(open)); |
|
| 80 | + | }); |
|
| 81 | + | document.addEventListener("click", function (e) { |
|
| 82 | + | if (!dd.contains(e.target)) { |
|
| 83 | + | menu.hidden = true; |
|
| 84 | + | toggle.setAttribute("aria-expanded", "false"); |
|
| 85 | + | } |
|
| 86 | + | }); |
|
| 87 | + | }); |
|
| 88 | + | document.querySelectorAll(".clone-copy").forEach(function (btn) { |
|
| 89 | + | btn.addEventListener("click", async function () { |
|
| 90 | + | try { |
|
| 91 | + | await navigator.clipboard.writeText(btn.dataset.clone); |
|
| 92 | + | var prev = btn.textContent; |
|
| 93 | + | btn.textContent = "copied"; |
|
| 94 | + | setTimeout(function () { btn.textContent = prev; }, 1500); |
|
| 95 | + | } catch (e) { |
|
| 96 | + | btn.textContent = "failed"; |
|
| 97 | + | } |
|
| 98 | + | }); |
|
| 99 | + | }); |
|
| 100 | + | </script> |
|
| 46 | 101 | {{end}} |
|