Compare commits

...

125 Commits

Author SHA1 Message Date
Joe Chen 0114e18bd3 31231 2026-05-28 17:20:19 -04:00
Joe Chen c4821cfe31 31231 2026-05-28 17:10:08 -04:00
Joe Chen 9327909209 1231 2026-05-28 16:53:38 -04:00
Joe Chen 8f9a1cf2c1 312 2026-05-28 16:40:47 -04:00
Joe Chen bc6c1ddf07 231 2026-05-28 16:31:27 -04:00
Joe Chen 65cce4eafe 312 2026-05-28 16:22:27 -04:00
Joe Chen 7102695655 213 2026-05-28 16:04:53 -04:00
Joe Chen 32573b844c 213 2026-05-28 16:00:32 -04:00
Joe Chen 98cb5a3db4 1231 2026-05-28 15:54:47 -04:00
Joe Chen a5d2c0ee36 111 2026-05-28 14:09:49 -04:00
Joe Chen 2d78fd22dc 213 2026-05-28 12:47:43 -04:00
Joe Chen c81f418856 1231 2026-05-28 12:25:48 -04:00
Joe Chen d259b87cfd 2131 2026-05-28 12:06:54 -04:00
Joe Chen f7449a8c85 21312 2026-05-28 12:00:53 -04:00
Joe Chen 9e1525bdf8 12312 2026-05-27 23:55:24 -04:00
unknwon c604d23be5 web(api): standardize repo handlers on flamego.Context
Drop `*http.Request` from handlers that only needed it for the request
context or `URL.Query()`, and replace `params := c.Params()` indexing
with `c.Param(...)` lookups. Every handler in webapi_repo.go now takes
just `(c flamego.Context, user *database.User)`.
2026-05-27 22:28:55 -04:00
Joe Chen 22f84f7ee9 111 2026-05-27 22:19:26 -04:00
unknwon c6b90ea5ab web(api): rename repo header fields to match GitHub-style naming
Apply review feedback on PR #8295. Flatten `repoHeaderCounts` into
`repoHeader` and rename fields to be more descriptive on the JSON wire
(`isViewerAdmin`, `issuesEnabled`, `pullRequestsEnabled`, `wikiEnabled`,
`watches`, `openPullRequests`, `isViewerWatching`, `hasViewerStarred`).
Apply the same naming to `repoActionResponse`. Rename `getRepoRaw` ->
`getRepoRawFile` with `{file}` param. Update the TS types and consumers
in `RepoHeader.tsx` to match.
2026-05-27 21:39:04 -04:00
Joe Chen 552fde5e8c 231 2026-05-27 21:25:27 -04:00
Joe Chen 87a7e5a80b 111 2026-05-27 16:48:31 -04:00
Joe Chen 6188e01752 111 2026-05-27 16:15:11 -04:00
Joe Chen 8c17948ddf 1231 2026-05-27 15:31:21 -04:00
Joe Chen a91b0551bd 111 2026-05-27 15:26:56 -04:00
Joe Chen 91e8a36578 1211 2026-05-27 14:20:54 -04:00
Joe Chen 5e191aa5c8 2131 2026-05-27 13:13:27 -04:00
unknwon c2a424a678 web(diff): tighten commit metadata row on commit page
Merge the author and parent/commit/buttons rows into a single
wrap-friendly flex line on desktop. Drop the committer line, since
showing it twice (author + committer) is rarely useful for the
common case where they match. Render each parent SHA as its own
clickable chip so multi-parent merge commits link to every parent.
Align View patch and Browse files to the left on mobile.
2026-05-27 09:47:15 -04:00
unknwon 10c829f798 web(diff): add per-file "Expand all lines" via raw file fetch
The commit diff page only ships the patch hunks, so unmodified context
between hunks is invisible. Add a per-file "Expand all lines" toggle so
the reader can pull in the full file when the surrounding code matters.

Backend:
- Migrate `repo.SingleDownload` to a new Flamego `getRepoRaw` handler.
  Same URL shape (`/{owner}/{name}/raw/{ref}/{path}`) so external
  consumers (`curl`, scripts) keep working. Bridged from the Macaron
  router via `flamegoBridger` so the legacy path doesn't double-route
  through `RepoRef` middleware. The ref segment accepts a branch, tag,
  or commit SHA; commit SHAs match first (the common case from the
  React diff page).
- Delete `repo.SingleDownload` and the legacy `m.Get("/raw/*", ...)`
  Macaron handler. `repo.ServeBlob` stays because `internal/route/api/v1`
  still uses it for the public REST API.

Frontend:
- Add an `UnfoldVertical` icon button to each file header. Click fetches
  the pre + post file contents in parallel via the legacy raw URL,
  calls `parseDiffFromFile` to upgrade the `FileDiffMetadata` to
  `isPartial: false`, and stores the result keyed by item id.
- The `items` useMemo swaps in the upgraded `fileDiff` when present and
  bumps the item `version` so Pierre's `CodeView` re-renders that file.
  Set `expandUnchanged: true` globally so non-partial files immediately
  render all context lines.
- Show a spinner during fetch, hide the button once expansion succeeds.
  Skip the button for added/deleted files (no opposite side to expand).
- Added/deleted files preserve the old behaviour (no expansion).
2026-05-27 03:51:16 -04:00
unknwon c535b64bb9 web: wire commit diff page to real data with TanStack Query
Migrate the React commit diff page off of mocked repo metadata and onto
live web API endpoints, and take over the legacy `/owner/repo/commit/{sha}`
URL so the React page is the canonical commit view.

Backend:
- Split `webapi.go` into `webapi.go` (shared infra), `webapi_user.go`
  (user handlers), and `webapi_repo.go` (repo handlers).
- Add `GET /api/web/{owner}/{name}/info` returning repo header data
  (avatar, visibility, counts, mirror, viewer state). Mirrors legacy
  `RepoAssignment` access logic: admin shortcut + partial-public masking.
- Add `GET /api/web/{owner}/{name}/commit/{sha}` returning commit
  metadata only. Patch text lives on the existing `.diff` URL so it
  avoids JSON-string escaping and caches independently.
- Migrate `repo.RawDiff` to Flamego `getRepoCommitRawDiff`. Now supports
  `?whitespace=` for the React diff toggle. Public URL unchanged.
- Add `POST/DELETE /api/web/{owner}/{name}/watch` and `.../star` returning
  the new viewer state + count so the client can update without refetch.
- Delete legacy `repo.Diff` and `repo.DiffJSON`. Add a SPA pass-through
  Macaron route at `/owner/repo/commit/{sha}` with the legacy
  `[a-f0-9]{7,40}` SHA regex.

Frontend:
- Install `@tanstack/react-query` and wire `QueryClientProvider` in
  `router.tsx`. Pass `queryClient` through router context so loaders can
  prefetch via `ensureQueryData`.
- Add `lib/queries/repo.ts` with `repoInfoQuery` + watch/star mutations.
- Move `CommitDiff.tsx` → `pages/repo/Commit.tsx` and `CommitDiff.search.ts`
  → `pages/repo/Commit.search.ts`. Rename `CommitDiff` → `RepoCommit`,
  `CommitDiffPage` → `RepoCommitPage`, etc.
- Change route from `/$owner/$repo/_diff/$sha` to
  `/$owner/$repo/commit/$sha`. Enforce SHA regex via TanStack `params.parse`
  and convert API 404s to router `notFound()` so they render the NotFound
  page instead of ServerError.
- Loader fetches metadata + raw diff in parallel (plus repo info via
  Query cache), assembles them into `RepoCommitPage`.
- Replace `RepoHeader`'s `RepoHeaderRepo` interface with the live
  `RepoInfo` type. Watch/Star buttons fire `useMutation` with optimistic
  cache updates via `setQueryData`. Anonymous users see sign-in links.
- Swap the "Public"/"Private" pill for a Globe/Lock icon with tooltip.
- Add a collapsible desktop file tree. The toolbar's "Showing N changed
  files" row owns a single toggle icon that opens the Sheet on mobile
  and toggles the persistent sidebar on desktop. State persists to
  localStorage.
- Hide the always-on "Verified" badge until commit signature
  verification lands.
2026-05-27 02:57:35 -04:00
Joe Chen c3577dc6fa 213 2026-05-26 13:18:29 -04:00
unknwon d7c3f16f7a web(diff): use PanelLeftOpen icon for mobile file-tree trigger 2026-05-26 12:02:25 -04:00
unknwon eb24142b83 web: polish commit diff page chrome and search UX
- DiffSearch: walk hunks by addition/deletionCount so matches on context
  lines and pure-deletion hunks are no longer dropped.
- DiffSearch: nudge popup up to top-1 so it sits closer to the toolbar.
- RepoHeader: add per-repo avatar slot (mocked to favicon for now),
  fold mobile tabs past the third into a hamburger overflow, swap
  Issues icon from Clock to CircleDot, nudge avatar down 2px to
  optically center the off-center favicon glyph.
- CommitDiff: render the authored timestamp as a relative string with
  RFC1123 tooltip (matches Gogs's TimeSince template helper); helper
  lives in web/src/lib/relative-time.ts.
- CommitDiff: inject GitHub-style yellow into Pierre's selected-line
  background overrides so search matches read clearly in both themes.
- AGENTS.md: note that chrome-devtools MCP should run headless.
2026-05-26 12:02:25 -04:00
unknwon dcad796c73 web: build out commit diff page on @pierre/diffs
Adds the full commit diff experience around the @pierre/diffs CodeView
and @pierre/trees FileTree spike from the prior commit:

- RepoHeader, DiffToolbar, FileHeaderMenu, ResizableSidebar components
  for the page chrome and per-file actions
- Sheet and Tooltip shadcn primitives
- CommitDiff.search.ts encodes diff toggles in the URL via TanStack
  Router validation so the view is shareable
- Sticky workspace lock that pins the toolbar plus tree plus diff to
  the viewport once the user scrolls past the commit metadata
- Whitespace mode wired through to git via the diff API's new
  whitespace query (ignore-all, ignore-change)
- Per-file collapse, status filter, unified/split toggle, wrap, expand
  all and collapse all
- New --color-success, --color-diff-added, --color-diff-removed
  tokens documented in DESIGN.md, replacing ad-hoc Tailwind palette
  references
2026-05-26 12:02:25 -04:00
Joe Chen 9b7d8ebd9d WIP: Pierre diff 2026-05-26 12:01:58 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 878caa7378 ci: notarize macOS release archives (#8297) 2026-05-24 23:08:45 -04:00
ᴊᴏᴇ ᴄʜᴇɴ adea243ee8 feat(web): migrate account activation page to React (#8296) 2026-05-24 22:35:41 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 44f0222a71 web: migrate /user/sign-out to Flamego (#8294) 2026-05-24 11:16:57 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 26483c41c6 feat(web): add React sign-up page with Flamego captcha (#8291) 2026-05-23 23:33:41 -04:00
dependabot[bot] 403db931cf mod: bump filippo.io/edwards25519 from 1.1.0 to 1.1.1 (#8292) 2026-05-23 22:25:56 -04:00
dependabot[bot] cd2f94a85b mod: bump github.com/redis/go-redis/v9 from 9.5.1 to 9.5.5 (#8293) 2026-05-23 22:25:41 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 4935e7a63b web: move password reset to React (#8290) 2026-05-23 21:55:22 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 71dfd3c7ac chore: fix up README image positions
[skip ci]
2026-05-22 16:31:17 -04:00
ᴊᴏᴇ ᴄʜᴇɴ ecb04beadd chore: align-center README images
[skip ci]
2026-05-22 16:28:22 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 83a48c286d fix: remove forgeable remember-me cookie, persist sessions by default (#8289) 2026-05-22 16:24:39 -04:00
ᴊᴏᴇ ᴄʜᴇɴ f739682e9a Move sign-in MFA step to React with /api/web/user/mfa (#8288) 2026-05-22 15:33:06 -04:00
ᴊᴏᴇ ᴄʜᴇɴ d54f98f5a4 feat(web): adopt Pierre theme palette and reorder sign-in tab stops (#8287) 2026-05-22 11:19:38 -04:00
ᴊᴏᴇ ᴄʜᴇɴ e7d0cb646d Update README banner for dark mode (#8286) 2026-05-22 00:56:13 -04:00
ᴊᴏᴇ ᴄʜᴇɴ dd6be39208 feat: React-based sign-in page with /api/web/user/sign-in (#8285) 2026-05-22 00:28:27 -04:00
ᴊᴏᴇ ᴄʜᴇɴ c93373baec feat: add /api/web/user/sign-out and nest user info under /user/info (#8284) 2026-05-21 15:05:19 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 90790b2966 Embed WebContext and add /api/web/user-info endpoint (#8282) 2026-05-21 14:43:09 -04:00
ᴊᴏᴇ ᴄʜᴇɴ e9310ea08f chore: remove Packager.io build support (#8281) 2026-05-20 22:55:08 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 75f99c9435 chore: remove Taskfile usage (#8280) 2026-05-20 19:22:50 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 343007e78a ci(moon): set default VCS branch to main (#8279) 2026-05-20 17:49:52 -04:00
ᴊᴏᴇ ᴄʜᴇɴ b67c13c6bb feat: introduce React web frontend and migrate home + 404 pages (#8276) 2026-05-20 17:45:31 -04:00
ᴊᴏᴇ ᴄʜᴇɴ a3c9f4acef Replace Docker latest on main with an edge tag (#8278) 2026-05-20 15:47:42 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 4c80cbc7eb refactor: inline disallowed-username regex in UsersStore.Authenticate (#8274) 2026-05-19 12:52:06 -04:00
ᴊᴏᴇ ᴄʜᴇɴ bfec14a857 refactor: render mail templates with html/template directly (#8272) 2026-05-19 12:20:13 -04:00
ᴊᴏᴇ ᴄʜᴇɴ cc8036e081 chore: remove DeepSource configuration (#8275)
[skip ci]
2026-05-19 12:11:37 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 765c0e96db refactor: replace toolbox middleware with a minimal /healthcheck handler (#8271) 2026-05-18 22:37:13 -04:00
ᴊᴏᴇ ᴄʜᴇɴ f3e563d854 refactor: prepare email package for web framework migration (#8270) 2026-05-18 22:22:46 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 199cf4fd5b security: don't follow redirects on webhook delivery (#8263) 2026-05-18 21:08:46 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 0089c4c8e5 auth: trust reverse proxy auth header only from configured proxies (#8264) 2026-05-18 13:42:46 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 6734dd46c3 docs: add changelog entry for GHSA-3qq3-668m-v9mj (#8266)
[skip ci]
2026-05-17 20:59:17 -04:00
ᴊᴏᴇ ᴄʜᴇɴ a5d3439e2d chore: replace reflect.Ptr with reflect.Pointer (#8262) 2026-05-17 20:39:09 -04:00
dependabot[bot] d7571322a0 mod: bump golang.org/x/image from 0.36.0 to 0.38.0 (#8218)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 16:43:28 -04:00
dependabot[bot] edc83e6ab2 mod: bump github.com/cockroachdb/errors from 1.12.0 to 1.13.0 (#8245)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 08:48:53 -04:00
dependabot[bot] 7297aee50d mod: bump github.com/Azure/go-ntlmssp from 0.0.0-20221128193559-754e69321358 to 0.1.1 (#8238)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 22:09:12 -04:00
dependabot[bot] 27e92f8463 mod: bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 (#8237)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 21:57:16 -04:00
dependabot[bot] b36ba5b60e mod: bump github.com/jackc/pgx/v5 from 5.6.0 to 5.9.0 (#8233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 18:58:35 -04:00
dependabot[bot] 84e23a403e mod: bump golang.org/x/crypto from 0.48.0 to 0.49.0 (#8221)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 16:20:18 -04:00
dependabot[bot] b53d316233 mod: bump gopkg.in/ini.v1 from 1.67.0 to 1.67.1 (#8193)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-15 14:59:48 -04:00
dependabot[bot] 47bff199cc mod: bump golang.org/x/crypto from 0.47.0 to 0.48.0 (#8192)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-15 14:58:42 -04:00
ᴊᴏᴇ ᴄʜᴇɴ 998512edfb ci: bump aquasecurity/trivy-action from 0.33.1 to 0.35.0 (#8204)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 13:58:53 -04:00
dependabot[bot] 1ed882b611 mod: bump golang.org/x/image from 0.35.0 to 0.36.0 (#8194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 12:49:44 -05:00
dependabot[bot] 391edc74c2 mod: bump golang.org/x/text from 0.33.0 to 0.34.0 (#8195)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 20:58:12 -05:00
dependabot[bot] 944bfeb57e mod: bump github.com/prometheus/client_golang from 1.23.0 to 1.23.2 (#8196)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 20:29:21 -05:00
ᴊᴏᴇ ᴄʜᴇɴ e8b6dea462 release: fix up sentence case for release name
[skip ci]
2026-02-18 22:45:51 -05:00
Joe Chen 2989605fd8 Update CHANGELOG for 0.14.2 release
[skip ci]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:22:40 -05:00
JSS df467d8ff1 Fix git reset --end-of-options error on file upload and edit (#8184) 2026-02-18 19:04:31 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 36d56d5525 all: rename packages ending with "util" to end with "x" (#8182)
Co-authored-by: JSS <jss@unknwon.dev>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 13:25:19 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 5f17b670b3 docker: pin Go 1.26 in builder stage images (#8180)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 23:44:13 -05:00
ᴊᴏᴇ ᴄʜᴇɴ ea682c5bbc all: upgrade to Go 1.26 (#8179)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 23:31:14 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 9001a68cdd js: use safe DOM construction for milestone and assignee selection (#8178)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 23:24:52 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 295bfba729 context: reject access tokens passed via URL query parameters (#8177) 2026-02-13 15:27:48 -05:00
ᴊᴏᴇ ᴄʜᴇɴ ac21150a53 template: escape untrusted names in locale strings piped through Safe (#8176)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-12 21:42:23 -05:00
ᴊᴏᴇ ᴄʜᴇɴ a000f0c7a6 database: use safe git-module API for tag deletion (#8175)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-12 21:24:44 -05:00
Joe Chen a976fd2f9c chore: minor case fixup
[skip ci]
2026-02-12 21:09:36 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 441c64d7bd markup: restrict data URI scheme to safe image MIME types (#8174)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 22:26:31 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 5c67d47512 database: remove MSSQL backend support (#8173) 2026-02-10 18:41:31 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 94d6e53dc2 email: replace gomail with go-mail (#8164)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 17:44:01 -05:00
ᴊᴏᴇ ᴄʜᴇɴ a1fa62b270 all: decouple API types from go-gogs-client SDK (#8171)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-10 10:56:17 -05:00
Copilot 317e28b908 Remove non-existent README_ZH.md from release workflow (#8170)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-02-09 09:22:24 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 069d3535d6 chore: fix broken links to gogs.io (#8169) 2026-02-09 09:09:59 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 81ee883644 lfs: verify content hash and prevent object overwrite (#8166)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2026-02-08 17:14:12 -05:00
Copilot 400ae7bd28 Add CLI reference doc page under advancing and normalize gogs command references (#8165)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-02-08 10:18:41 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 630ae0b3b0 cmd: migrate from urfave/cli v1 to v3 (#8160) 2026-02-08 00:58:05 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 3c358ede6d all: migrate from json-iterator to encoding/json (#8159) 2026-02-08 00:34:36 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 48500aa2b0 all: migrate from satori/go.uuid to google/uuid (#8161) 2026-02-08 00:13:43 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 785157ba1f all: migrate from nfnt/resize to golang.org/x/image/draw (#8158) 2026-02-08 00:08:50 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 1c8016a27b database/schemadoc: migrate to github.com/DATA-DOG/go-sqlmock (#8157) 2026-02-08 00:07:45 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 00c36d8d8a public: update jQuery from 3.6.0 to 3.7.1 (#8156) 2026-02-07 23:48:49 -05:00
Copilot 3747cd9058 Fix broken links in docs/getting-started/introduction.mdx (#8155)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-02-07 18:19:02 -05:00
Joe Chen 08e7cfd76c docs: fix up wwads-cn style in dark mode
[skip ci]
2026-02-07 17:49:58 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 9dd3e58f7b docs: migrate to Mintlify (#8154) 2026-02-07 17:32:52 -05:00
ᴊᴏᴇ ᴄʜᴇɴ edc1478f6b cmd: remove cert subcommand (#8153)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:23:33 -05:00
ᴊᴏᴇ ᴄʜᴇɴ bb86d12c36 cmd: show detected config path in web command help (#8152)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 13:38:53 -05:00
Copilot bf17cc6c69 Replace github.com/unknwon/com with stdlib and internal helpers (#8148)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Joe Chen <jc@unknwon.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 22:08:54 -05:00
Copilot 6d56105f8f Run modernize tool across codebase (#8147)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Joe Chen <jc@unknwon.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2026-02-05 21:32:09 -05:00
Joe Chen ed5d02e036 chore: free up "docs/" subdir for Mintlify
[skip ci]
2026-02-01 23:15:47 -05:00
dependabot[bot] 5874791a57 mod: bump golang.org/x/crypto from 0.45.0 to 0.47.0 (#8141)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 09:20:24 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 17ad3d3425 pkgr: fix up install main package path (#8146) 2026-02-01 09:12:12 -05:00
dependabot[bot] 9b2a967e45 mod: bump github.com/editorconfig/editorconfig-core-go/v2 from 2.6.3 to 2.6.4 (#8143)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 09:01:09 -05:00
dependabot[bot] e80635a449 mod: bump github.com/go-ldap/ldap/v3 from 3.4.11 to 3.4.12 (#8144)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 09:00:50 -05:00
Joe Chen 47bccf292d pkgr: fix up install path
[skip ci]
2026-02-01 08:53:52 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 38def73489 Update security policy for version support and advisories
[skip ci]
2026-02-01 08:16:22 -05:00
Copilot 6cf6422b88 Remove codecov config and upload in CI (#8145)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: unknwon <2946214+unknwon@users.noreply.github.com>
2026-02-01 08:10:55 -05:00
dependabot[bot] 8d8d66ec1d mod: bump github.com/olekukonko/tablewriter from 1.1.0 to 1.1.3 (#8140)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-01 07:45:08 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 7ea2c4093f refactor: move main package from root to cmd/gogs (#8139)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:28:48 -05:00
dependabot[bot] 7ebfb202e4 mod: bump github.com/olekukonko/tablewriter from 0.0.5 to 1.1.0 (#8039)
Co-authored-by: Joe Chen <jc@unknwon.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:11:08 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 7b8c560f15 ci(go): use tparse for test output in non-Windows jobs (#8138)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:58:41 -05:00
ᴊᴏᴇ ᴄʜᴇɴ a636dcf678 fix(release): remove "v" prefix from archive names (#8137)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:42:45 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 3dcb74be39 release: cut CHANGELOG entries for 0.14.1
[skip ci]
2026-01-31 22:23:09 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 7ad425025e fix(ssh): git clone via built-in SSH server hangs (#8135)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 22:20:43 -05:00
ᴊᴏᴇ ᴄʜᴇɴ 85abee4b9b chore: update release templates (#8134)
[skip ci]
2026-01-31 20:08:38 -05:00
Joe Chen 997c3c5eab chore: take care of legacy code 2026-01-31 20:07:49 -05:00
Joe Chen a5ddb2665e chore: bump dev version to 0.15
[skip ci]
2026-01-31 20:04:55 -05:00
550 changed files with 30296 additions and 5345 deletions
+13
View File
@@ -0,0 +1,13 @@
Analyze and help fix the GitHub Security Advisory (GHSA) at: $ARGUMENTS
Steps:
1. Fetch the GHSA page using `gh api repos/gogs/gogs/security-advisories` and understand the vulnerability details (description, severity, affected versions, CWE).
2. Verify the reported vulnerability actually exists, and why.
3. Identify the affected code in this repository.
4. Propose a fix with a clear explanation of the root cause and how the fix addresses it. Check for prior art in the codebase to stay consistent with existing patterns.
5. Implement the fix. Only add tests when there is something meaningful to test at our layer.
6. Run all the usual build and test commands.
7. If a changelog entry is warranted (user will specify), add it to CHANGELOG.md with a placeholder for the PR link.
8. Create a branch named after the GHSA ID, commit, and push.
9. Create a pull request with a proper title and description, do not reveal too much detail and link the GHSA.
10. If a changelog entry was added, update it with the PR link, then commit and push again.
-26
View File
@@ -1,26 +0,0 @@
version = 1
exclude_patterns = ["**/mocks_test.go"]
[[analyzers]]
name = "docker"
enabled = true
[[analyzers]]
name = "shell"
enabled = true
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_root = "github.com/gogs/gogs"
[[transformers]]
name = "gofumpt"
enabled = true
[[transformers]]
name = "gofmt"
enabled = true
+5 -4
View File
@@ -1,5 +1,3 @@
.packager
.packager/**
scripts
scripts/**
.github/
@@ -11,5 +9,8 @@ scripts/**
.gitignore
Dockerfile*
gogs
!Taskfile.yml
node_modules
**/node_modules
public/dist
**/*.tsbuildinfo
**/.vite
+3 -4
View File
@@ -29,7 +29,7 @@ In addition to the general guides with open source contributions, you would also
### Ask for help
Before opening an issue, please make sure the problem you're encountering isn't already addressed on the [Troubleshooting](https://gogs.io/docs/intro/troubleshooting.html) and [FAQs](https://gogs.io/docs/intro/faqs.html) pages.
Before opening an issue, please make sure the problem you're encountering isn't already addressed on the [Troubleshooting](https://gogs.io/asking/troubleshooting) and [FAQs](https://gogs.io/asking/faq) pages.
### Create a new issue
@@ -65,13 +65,12 @@ Contributing to another codebase is not as simple as code changes, it is also ab
### Things we do not accept
1. Updates to locale files (`conf/locale_xx-XX.ini`) other than the `conf/locale_en-US.ini`. Please read the [guide for localizing Gogs](https://gogs.io/docs/features/i18n).
1. Updates to locale files (`conf/locale_xx-XX.ini`) other than the `conf/locale_en-US.ini`. Please read the [guide for localizing Gogs](https://gogs.io/advancing/localization).
1. Docker compose files.
### Coding guidelines
1. Please read the Sourcegraph's [Go style guide](https://docs.sourcegraph.com/dev/background-information/languages/go).
1. **NO** direct modifications to `.css` files, `.css` files are all generated by `.less` files. You can regenerate `.css` files by executing `task less`.
1. Please read the Sourcegraph's [Go style guide](https://github.com/sourcegraph/sourcegraph-public-snapshot/blob/main/doc/dev/background-information/languages/go.md).
## Your PR is merged!
@@ -22,16 +22,14 @@ On the `main` branch:
On the release branch:
- [ ] [Update the hard-coded version](https://github.com/gogs/gogs/commit/f17e7d5a2c36c52a1121d2315f3d75dcd8053b89) to the current release, e.g. `0.14.0+dev` -> `0.14.0`.
- [ ] [Update the hard-coded version](https://github.com/gogs/gogs/commit/f0e3cd90f8d7695960eeef2e4e54b2e717302f6c) to the current release, e.g. `0.14.0+dev` -> `0.14.0`.
- [ ] Wait for GitHub Actions to complete and no failed jobs.
- [ ] Publish new RC releases (e.g. `v0.14.0-rc.1`, `v0.14.0-rc.2`) to ensure Docker and release workflows both succeed.
- ⚠️ **Make sure the tag is created on the release branch**.
- [ ] Publish new RC releases (e.g. `v0.14.0-rc.1`, `v0.14.0-rc.2`) ⚠️ **on the release branch** ⚠️ and ensure Docker and release workflows both succeed.
- [ ] Pull down the Docker image and [run through application setup](https://github.com/gogs/gogs/blob/main/docker/README.md) to make sure nothing blows up.
- [ ] Download one of the release archives and run through application setup to make sure nothing blows up.
- [ ] Publish a new [GitHub release](https://github.com/gogs/gogs/releases) with entries from [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) for the current minor release.
- ⚠️ **Make sure the tag is created on the release branch**.
- [ ] [Wait for a new image tag for the current release](https://github.com/gogs/gogs/actions/workflows/docker.yml?query=event%3Arelease) to be created automatically on both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs).
- [ ] [Push a new Docker image tag](https://github.com/gogs/gogs/blob/main/docs/dev/release/release_new_version.md#update-docker-image-tag) as `<MAJOR>.<MINOR>` to both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs), e.g.:
- [ ] Publish a new [GitHub release](https://github.com/gogs/gogs/releases) ⚠️ **on the release branch** ⚠️ with entries from [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) for the current minor release.
- [ ] [Wait for new image tags for the current release](https://github.com/gogs/gogs/actions/workflows/docker.yml?query=event%3Arelease) to be created automatically on both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs).
- Pull down the Docker image and [run through application setup](https://github.com/gogs/gogs/blob/main/docker/README.md) to make sure nothing blows up.
- [ ] Download all release archives and [generate SHA256 checksum](https://github.com/gogs/gogs/blob/main/docs/dev/release/sha256.sh) for all binaries to the file `checksum_sha256.txt`.
- [ ] Upload all archives and `checksum_sha256.txt` to https://dl.gogs.io.
@@ -39,11 +37,10 @@ On the release branch:
On the `main` branch:
- [ ] Publish [GitHub security advisories](https://github.com/gogs/gogs/security) for security patches included in the release.
- [ ] Update the repository mirror on [Gitee](https://gitee.com/unknwon/gogs).
- [ ] Create a new release announcement in [Discussions](https://github.com/gogs/gogs/discussions/categories/announcements).
- [ ] Send a tweet on the [official Twitter account](https://twitter.com/GogsHQ) for the minor release.
- [ ] Close the milestone for the minor release.
- [ ] [Bump the hard-coded version](https://github.com/gogs/gogs/commit/a98968436cd5841cf691bb0b80c54c81470d1676) to the new develop version, e.g. `0.14.0+dev` -> `0.15.0+dev`.
- [ ] Run `task legacy` to identify deprecated code that is aimed to be removed in current develop version.
- [ ] Run `grep -rnw "\(LEGACY\|Deprecated\)" internal` to identify deprecated code that is aimed to be removed in current develop version.
- [ ] **After 14 days**, publish [GitHub security advisories](https://github.com/gogs/gogs/security) for security patches included in the release.
@@ -13,7 +13,7 @@ _This is generated from the [patch release template](https://github.com/gogs/gog
On the release branch:
- [ ] Make sure all commits are cherry-picked from the `main` branch by checking the patch milestone.
- Run `task build` for every cherry-picked commit to make sure there is no compilation error.
- Run `moon run gogs:build-prod --force` for every cherry-picked commit to make sure there is no compilation error.
- [ ] [Update CHANGELOG on the `main` branch](https://github.com/gogs/gogs/commit/f1102a7a7c545ec221d2906f02fa19170d96f96d) to include entries for the current patch release.
## During release
@@ -22,19 +22,16 @@ On the release branch:
- [ ] [Update the hard-coded version](https://github.com/gogs/gogs/commit/f0e3cd90f8d7695960eeef2e4e54b2e717302f6c) to the current release, e.g. `0.12.0` -> `0.12.1`.
- [ ] Wait for GitHub Actions to complete and no failed jobs.
- [ ] Publish new RC releases in [GitHub release](https://github.com/gogs/gogs/releases) (e.g. `v0.12.0-rc.1`, `v0.12.0-rc.2`) to ensure Docker workflow succeeds.
- ⚠️ **Make sure the tag is created on the release branch**.
- Pull down the Docker image and [run through application setup](https://github.com/gogs/gogs/blob/main/docker/README.md) to make sure nothing blows up.
- [ ] Publish new RC releases in [GitHub release](https://github.com/gogs/gogs/releases) (e.g. `v0.12.0-rc.1`, `v0.12.0-rc.2`) ⚠️ **on the release branch** ⚠️ and ensure Docker workflow succeeds.
- [ ] Pull down the Docker image and [run through application setup](https://github.com/gogs/gogs/blob/main/docker/README.md) to make sure nothing blows up.
- [ ] Download one of the release archives and run through application setup to make sure nothing blows up.
- [ ] Publish a new [GitHub release](https://github.com/gogs/gogs/releases) with entries from [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) for the current patch release and all previous releases with same minor version.
- ⚠️ **Make sure the tag is created on the release branch**.
- [ ] Publish a new [GitHub release](https://github.com/gogs/gogs/releases) ⚠️ **on the release branch** ⚠️ with entries from [CHANGELOG](https://github.com/gogs/gogs/blob/main/CHANGELOG.md) for the current patch release and all previous releases with same minor version.
- [ ] Update all previous GitHub releases with same minor version with the warning:
```
**️ Heads up! There is a new patch release [0.12.1](https://github.com/gogs/gogs/releases/tag/v0.12.1) available, we recommend directly installing or upgrading to that version.**
```
- [ ] [Wait for a new image tag for the current release](https://github.com/gogs/gogs/actions/workflows/docker.yml?query=event%3Arelease) to be created automatically on both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs).
- [ ] [Wait for new image tags for the current release](https://github.com/gogs/gogs/actions/workflows/docker.yml?query=event%3Arelease) to be created automatically on both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs).
- Pull down the Docker image and [run through application setup](https://github.com/gogs/gogs/blob/main/docker/README.md) to make sure nothing blows up.
- [ ] [Update Docker image tag](https://www.notion.so/jcunknwon/Cheatsheet-and-playbooks-c3b053da42114411bd27285cd065b2a6?source=copy_link#1654f105c63f80958d96cd72e2f5df69) for the minor release `<MAJOR>.<MINOR>` on both [Docker Hub](https://hub.docker.com/r/gogs/gogs/tags) and [GitHub Container registry](https://github.com/gogs/gogs/pkgs/container/gogs).
- [ ] Download all release archives and [generate SHA256 checksum](https://github.com/gogs/gogs/blob/main/docs/dev/release/sha256.sh) for all binaries to the file `checksum_sha256.txt`.
- [ ] Upload all archives and `checksum_sha256.txt` to https://dl.gogs.io.
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
- name: Check out repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# We must fetch at least the immediate parents so that if this is
+72 -77
View File
@@ -15,65 +15,6 @@ on:
types: [ published ]
jobs:
buildx:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'gogs/gogs' }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Inspect builder
run: |
echo "Name: ${{ steps.buildx.outputs.name }}"
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
echo "Status: ${{ steps.buildx.outputs.status }}"
echo "Flags: ${{ steps.buildx.outputs.flags }}"
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
gogs/gogs:latest
ghcr.io/gogs/gogs:latest
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: gogs/gogs:latest
exit-code: '1'
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
if: ${{ failure() }}
with:
smtp_username: ${{ secrets.SMTP_USERNAME }}
smtp_password: ${{ secrets.SMTP_PASSWORD }}
buildx-next:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'gogs/gogs' }}
concurrency:
@@ -85,7 +26,7 @@ jobs:
contents: read
packages: write
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
@@ -126,13 +67,13 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
gogs/gogs:next-latest
ghcr.io/gogs/gogs:next-latest
registry.digitalocean.com/gogs/gogs:next-latest
gogs/gogs:edge
ghcr.io/gogs/gogs:edge
registry.digitalocean.com/gogs/gogs:edge
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: gogs/gogs:next-latest
image-ref: gogs/gogs:edge
exit-code: '1'
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
@@ -173,7 +114,7 @@ jobs:
permissions:
contents: read
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Docker Buildx
id: buildx
@@ -201,7 +142,7 @@ jobs:
tags: |
ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:7d
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ttl.sh/gogs/gogs-${{ steps.short-sha.outputs.sha }}:7d
exit-code: '1'
@@ -212,7 +153,7 @@ jobs:
permissions:
contents: read
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Docker Buildx
id: buildx
@@ -241,7 +182,7 @@ jobs:
tags: |
ttl.sh/gogs/gogs-next-${{ steps.short-sha.outputs.sha }}:7d
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ttl.sh/gogs/gogs-next-${{ steps.short-sha.outputs.sha }}:7d
exit-code: '1'
@@ -255,6 +196,13 @@ jobs:
contents: read
packages: write
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Full history with tags is required so the next step can determine
# whether this release is the highest stable version across the repo.
fetch-depth: 0
fetch-tags: true
- name: Compute image tags
run: |
IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -c 2-)
@@ -263,19 +211,34 @@ jobs:
TAGS="gogs/gogs:$IMAGE_TAG
ghcr.io/gogs/gogs:$IMAGE_TAG"
# Add minor version tag for stable releases (no prerelease suffix per semver).
# For stable releases (no prerelease suffix per semver), add the
# minor-version tag only if this release is the highest patch of that
# minor line, and add `latest` only if this release is the highest
# stable version across the repository. Back-patches on older lines
# must not clobber moving tags.
if [[ ! "$IMAGE_TAG" =~ - ]]; then
STABLE_TAGS=$(git tag --list 'v*' | sed 's/^v//' | grep -v -- '-' || true)
HIGHEST_STABLE=$(echo "$STABLE_TAGS" | sort -V | tail -n 1)
MINOR_TAG=$(echo "$IMAGE_TAG" | cut -d. -f1,2)
TAGS="$TAGS
# `|| true` keeps the step running when `grep` finds no matches,
# since bash runs with `-e -o pipefail` in GitHub Actions.
HIGHEST_IN_MINOR=$(echo "$STABLE_TAGS" | { grep "^${MINOR_TAG}\." || true; } | sort -V | tail -n 1)
if [[ "$IMAGE_TAG" == "$HIGHEST_IN_MINOR" ]]; then
TAGS="$TAGS
gogs/gogs:$MINOR_TAG
ghcr.io/gogs/gogs:$MINOR_TAG"
fi
if [[ "$IMAGE_TAG" == "$HIGHEST_STABLE" ]]; then
TAGS="$TAGS
gogs/gogs:latest
ghcr.io/gogs/gogs:latest"
fi
fi
echo "TAGS<<EOF" >> $GITHUB_ENV
echo "$TAGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
@@ -308,6 +271,11 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ env.TAGS }}
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: gogs/gogs:${{ env.IMAGE_TAG }}
exit-code: '1'
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
if: ${{ failure() }}
@@ -324,6 +292,13 @@ jobs:
contents: read
packages: write
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Full history with tags is required so the next step can determine
# whether this release is the highest stable version across the repo.
fetch-depth: 0
fetch-tags: true
- name: Compute image tags
run: |
IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -c 2-)
@@ -332,19 +307,34 @@ jobs:
TAGS="gogs/gogs:next-$IMAGE_TAG
ghcr.io/gogs/gogs:next-$IMAGE_TAG"
# Add minor version tag for stable releases (no prerelease suffix per semver).
# For stable releases (no prerelease suffix per semver), add the
# minor-version tag only if this release is the highest patch of that
# minor line, and add `next-latest` only if this release is the
# highest stable version across the repository. Back-patches on older
# lines must not clobber moving tags.
if [[ ! "$IMAGE_TAG" =~ - ]]; then
STABLE_TAGS=$(git tag --list 'v*' | sed 's/^v//' | grep -v -- '-' || true)
HIGHEST_STABLE=$(echo "$STABLE_TAGS" | sort -V | tail -n 1)
MINOR_TAG=$(echo "$IMAGE_TAG" | cut -d. -f1,2)
TAGS="$TAGS
# `|| true` keeps the step running when `grep` finds no matches,
# since bash runs with `-e -o pipefail` in GitHub Actions.
HIGHEST_IN_MINOR=$(echo "$STABLE_TAGS" | { grep "^${MINOR_TAG}\." || true; } | sort -V | tail -n 1)
if [[ "$IMAGE_TAG" == "$HIGHEST_IN_MINOR" ]]; then
TAGS="$TAGS
gogs/gogs:next-$MINOR_TAG
ghcr.io/gogs/gogs:next-$MINOR_TAG"
fi
if [[ "$IMAGE_TAG" == "$HIGHEST_STABLE" ]]; then
TAGS="$TAGS
gogs/gogs:next-latest
ghcr.io/gogs/gogs:next-latest"
fi
fi
echo "TAGS<<EOF" >> $GITHUB_ENV
echo "$TAGS" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
@@ -378,6 +368,11 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ env.TAGS }}
- name: Scan for container vulnerabilities
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: gogs/gogs:next-${{ env.IMAGE_TAG }}
exit-code: '1'
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
if: ${{ failure() }}
+24 -29
View File
@@ -29,26 +29,22 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.25.x
- name: Install Task
uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
go-version: 1.26.x
- name: Check Go module tidiness and generated files
shell: bash
run: |
go mod tidy
task generate
go generate ./...
STATUS=$(git status --porcelain)
if [ ! -z "$STATUS" ]; then
echo "Unstaged files:"
echo $STATUS
echo "Run 'go mod tidy' or 'task generate' commit them"
echo "Run 'go mod tidy' or 'go generate ./...' and commit them"
exit 1
fi
- name: Run golangci-lint
@@ -61,23 +57,21 @@ jobs:
name: Test
strategy:
matrix:
go-version: [ 1.25.x ]
go-version: [ 1.26.x ]
platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
file: ./coverage
flags: unittests
run: |
go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic -json ./... > test-report.json
go install github.com/mfridman/tparse@latest
tparse -all -file=test-report.json
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
if: ${{ failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
@@ -91,11 +85,11 @@ jobs:
name: Test Windows
strategy:
matrix:
go-version: [ 1.25.x ]
go-version: [ 1.26.x ]
platform: [ windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
@@ -103,11 +97,6 @@ jobs:
go-version: ${{ matrix.go-version }}
- name: Run tests with coverage
run: go test -shuffle=on -v -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
file: ./coverage
flags: unittests
- name: Send email on failure
uses: unknwon/send-email-on-failure@89339a1bc93f4ad1d30f3b7e4911fcba985c9adb # v1
if: ${{ failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
@@ -119,7 +108,7 @@ jobs:
name: Postgres
strategy:
matrix:
go-version: [ 1.25.x ]
go-version: [ 1.26.x ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
services:
@@ -135,14 +124,17 @@ jobs:
ports:
- 5432:5432
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./internal/database/...
run: |
go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic -json ./internal/database/... > test-report.json
go install github.com/mfridman/tparse@latest
tparse -all -file=test-report.json
env:
GOGS_DATABASE_TYPE: postgres
PGPORT: 5432
@@ -155,20 +147,23 @@ jobs:
name: MySQL
strategy:
matrix:
go-version: [ 1.25.x ]
go-version: [ 1.26.x ]
platform: [ ubuntu-22.04 ] # Use the lowest version possible for backwards compatibility
runs-on: ${{ matrix.platform }}
steps:
- name: Start MySQL server
run: sudo systemctl start mysql
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Install Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Run tests with coverage
run: go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic ./internal/database/...
run: |
go test -shuffle=on -v -race -coverprofile=coverage -covermode=atomic -json ./internal/database/... > test-report.json
go install github.com/mfridman/tparse@latest
tparse -all -file=test-report.json
env:
GOGS_DATABASE_TYPE: mysql
MYSQL_USER: root
+95 -25
View File
@@ -23,34 +23,46 @@ permissions:
jobs:
build:
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}${{ matrix.suffix }}
runs-on: ubuntu-latest
if: ${{ github.repository == 'gogs/gogs' }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- {goos: linux, goarch: amd64}
- {goos: linux, goarch: arm64}
- {goos: linux, goarch: "386"}
- {goos: darwin, goarch: amd64}
- {goos: darwin, goarch: arm64}
- {goos: windows, goarch: amd64}
- {goos: windows, goarch: arm64}
- {goos: windows, goarch: "386"}
- {goos: windows, goarch: amd64, suffix: "_mws", tags: minwinsvc}
- {goos: windows, goarch: arm64, suffix: "_mws", tags: minwinsvc}
- {goos: windows, goarch: "386", suffix: "_mws", tags: minwinsvc}
- {goos: linux, goarch: amd64, runner: ubuntu-latest}
- {goos: linux, goarch: arm64, runner: ubuntu-latest}
- {goos: linux, goarch: "386", runner: ubuntu-latest}
- {goos: darwin, goarch: amd64, runner: macos-latest}
- {goos: darwin, goarch: arm64, runner: macos-latest}
- {goos: windows, goarch: amd64, runner: ubuntu-latest}
- {goos: windows, goarch: arm64, runner: ubuntu-latest}
- {goos: windows, goarch: "386", runner: ubuntu-latest}
- {goos: windows, goarch: amd64, suffix: "_mws", tags: minwinsvc, runner: ubuntu-latest}
- {goos: windows, goarch: arm64, suffix: "_mws", tags: minwinsvc, runner: ubuntu-latest}
- {goos: windows, goarch: "386", suffix: "_mws", tags: minwinsvc, runner: ubuntu-latest}
steps:
- name: Checkout code
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Go
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: 1.25.x
go-version: 1.26.x
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
- name: Build web assets
run: |
pnpm install --frozen-lockfile
pnpm --filter gogs-web run build
- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "version=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
echo "version=${{ github.event.release.tag_name }}" | sed 's/version=v/version=/' >> "$GITHUB_OUTPUT"
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
@@ -70,9 +82,9 @@ jobs:
BINARY_NAME="gogs.exe"
fi
TAGS_FLAG=""
TAGS="prod"
if [ -n "${{ matrix.tags }}" ]; then
TAGS_FLAG="-tags ${{ matrix.tags }}"
TAGS="$TAGS ${{ matrix.tags }}"
fi
go build -v \
@@ -80,8 +92,44 @@ jobs:
-X \"gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')\"
-X \"gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)\"
" \
$TAGS_FLAG \
-trimpath -o "$BINARY_NAME"
-tags "$TAGS" \
-trimpath -o "$BINARY_NAME" ./cmd/gogs
- name: Import Apple signing certificate
if: ${{ matrix.goos == 'darwin' }}
env:
APPLE_DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_BASE64 }}
APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
run: |
if [ -z "$APPLE_DEVELOPER_ID_CERTIFICATE_BASE64" ] || [ -z "$APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD" ] || [ -z "$APPLE_KEYCHAIN_PASSWORD" ]; then
echo "Missing required Apple signing secrets." >&2
exit 1
fi
CERTIFICATE_PATH="$RUNNER_TEMP/developer_id_application.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
printf '%s' "$APPLE_DEVELOPER_ID_CERTIFICATE_BASE64" | base64 -d > "$CERTIFICATE_PATH"
security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -P "$APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Sign macOS binary
if: ${{ matrix.goos == 'darwin' }}
env:
APPLE_DEVELOPER_IDENTITY: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}
run: |
if [ -z "$APPLE_DEVELOPER_IDENTITY" ]; then
echo "Missing required Apple signing identity secret." >&2
exit 1
fi
security find-identity -v -p codesigning
codesign --force --options runtime --timestamp --sign "$APPLE_DEVELOPER_IDENTITY" "gogs"
codesign --verify --verbose=2 "gogs"
- name: Prepare archive contents
run: |
mkdir -p dist/gogs
@@ -90,7 +138,7 @@ jobs:
BINARY_NAME="gogs.exe"
fi
cp "$BINARY_NAME" dist/gogs/
cp LICENSE README.md README_ZH.md dist/gogs/
cp LICENSE README.md dist/gogs/
cp -r scripts dist/gogs/
- name: Create archives
working-directory: dist
@@ -103,6 +151,28 @@ jobs:
if [ "${{ matrix.goos }}" = "linux" ]; then
tar -czvf "${ARCHIVE_BASE}.tar.gz" gogs
fi
- name: Notarize macOS archive
if: ${{ matrix.goos == 'darwin' }}
env:
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
APPLE_NOTARY_KEY_BASE64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
run: |
if [ -z "$APPLE_NOTARY_ISSUER_ID" ] || [ -z "$APPLE_NOTARY_KEY_BASE64" ] || [ -z "$APPLE_NOTARY_KEY_ID" ]; then
echo "Missing required Apple notarization secrets." >&2
exit 1
fi
VERSION="${{ steps.version.outputs.version }}"
ARCHIVE_PATH="dist/gogs_${VERSION}_${{ matrix.goos }}_${{ matrix.goarch }}${{ matrix.suffix }}.zip"
NOTARY_KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_NOTARY_KEY_ID}.p8"
printf '%s' "$APPLE_NOTARY_KEY_BASE64" | base64 -d > "$NOTARY_KEY_PATH"
xcrun notarytool submit "$ARCHIVE_PATH" \
--key "$NOTARY_KEY_PATH" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_ISSUER_ID" \
--wait
- name: Upload to release
env:
GH_TOKEN: ${{ github.token }}
@@ -112,14 +182,14 @@ jobs:
if [ "${{ github.event_name }}" != "release" ]; then
git tag -f "$RELEASE_TAG"
git push origin "$RELEASE_TAG" --force || true
RELEASE_TITLE="Release Archive Testing"
RELEASE_TITLE="Release archive testing"
RELEASE_NOTES="Automated testing release for workflow development."
if [ "$RELEASE_TAG" = "latest-commit-build" ]; then
RELEASE_TITLE="Latest Commit Build"
RELEASE_TITLE="Latest commit build"
RELEASE_NOTES="Automated build from the latest commit on main branch. This release is updated automatically with every push to main."
fi
gh release view "$RELEASE_TAG" || gh release create "$RELEASE_TAG" --title "$RELEASE_TITLE" --notes "$RELEASE_NOTES" --prerelease
fi
+54
View File
@@ -0,0 +1,54 @@
name: Web
on:
push:
branches:
- main
- 'release/**'
paths:
- 'web/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- 'conf/locale/locale_*.ini'
- '.github/workflows/web.yml'
pull_request:
paths:
- 'web/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- 'conf/locale/locale_*.ini'
- '.github/workflows/web.yml'
permissions:
contents: read
jobs:
web:
name: Lint and build
runs-on: ubuntu-latest
env:
MOON_COLOR: "true"
FORCE_COLOR: "1"
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: pnpm
- name: Set up moon
uses: moonrepo/setup-toolchain@261c62cb5b0f580c7be7c8cd0f023a2e96756095 # v0.6.4
with:
auto-install: false
- name: Lint
run: moon run web:lint
- name: Build
run: moon run web:build
- name: Check for uncommitted changes
run: git diff --exit-code --stat
+5 -2
View File
@@ -1,6 +1,10 @@
# Build artifacts
# Build artifacts and cache
.bin/
dist/
.moon/cache/
node_modules/
.agents/skills/
.claude/skills
# Runtime data
log/
@@ -9,7 +13,6 @@ data/
# Configuration and application files
.idea/
.task/
.envrc
# System junk
+4
View File
@@ -0,0 +1,4 @@
$schema: "https://moonrepo.dev/schemas/tasks.json"
taskOptions:
outputStyle: "stream"
+9
View File
@@ -0,0 +1,9 @@
$schema: "https://moonrepo.dev/schemas/workspace.json"
projects:
gogs: "."
web: "web"
vcs:
client: "git"
defaultBranch: "main"
-1
View File
@@ -1 +0,0 @@
main
-1
View File
@@ -1 +0,0 @@
web: ./gogs web -p ${PORT:=3000}
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
set -e
APP_NAME="gogs"
CLI="${APP_NAME}"
APP_USER=$(${CLI} config:get APP_USER)
APP_GROUP=$(${CLI} config:get APP_GROUP)
APP_CONFIG="/etc/${APP_NAME}/conf/app.ini"
mkdir -p "$(dirname ${APP_CONFIG})"
chown "${APP_USER}"."${APP_GROUP}" "$(dirname ${APP_CONFIG})"
[ -f ${APP_CONFIG} ] || ${CLI} run cp conf/app.ini ${APP_CONFIG}
${CLI} config:set USER="${APP_USER}"
sed -i "s|RUN_USER = git|RUN_USER = ${APP_USER}|" ${APP_CONFIG}
sed -i "s|RUN_MODE = dev|RUN_MODE = prod|" ${APP_CONFIG}
${CLI} config:set GOGS_CUSTOM=/etc/${APP_NAME}
# scale
${CLI} scale web=1 || true
# restart the service
service gogs restart || true
-35
View File
@@ -1,35 +0,0 @@
targets:
debian-11: &debian
build_dependencies:
- libpam0g-dev
dependencies:
- libpam0g
- git
debian-12:
<<: *debian
debian-13:
<<: *debian
debian-14:
<<: *debian
ubuntu-18.04:
<<: *debian
ubuntu-20.04:
<<: *debian
ubuntu-22.04:
<<: *debian
ubuntu-24.04:
<<: *debian
centos-9:
build_dependencies:
- pam-devel
# required for Go buildpack
- perl-Digest-SHA
dependencies:
- pam
- git
before:
- mv .packager/Procfile .
after:
- mv bin/gogs gogs
after_install: ./.packager/hooks/postinst
buildpack: https://github.com/heroku/heroku-buildpack-go.git#main
+20 -4
View File
@@ -9,24 +9,40 @@ This applies to all texts, including but not limited to UI, documentation, code
- Use sentence case. Preserve original casing for brand names.
- End with a period for a full sentence.
- Never use em dashes (`—`) or en dashes (``) in prose. Rewrite the sentence with a comma, period, colon, or parentheses instead. Exception: em/en dashes are allowed as visual separators in UI design (e.g., between a title and description, in a terminal prompt label) where they function as a graphic element rather than punctuation.
- Do not overuse semicolons. Two short sentences are almost always clearer than one sentence joined by a semicolon. Reserve the semicolon for the rare case where the two clauses are so tightly coupled that splitting them loses meaning, never as a default em-dash replacement or a way to chain related thoughts.
- Do not add comments that repeat what the code is doing, always prefer more descriptive names. Do add comments for intentions that aren't obvious via reading the code alone. This rule takes precedence over matching existing patterns.
## Coding guidelines
- Use `github.com/cockroachdb/errors` for error handling.
- Use `github.com/stretchr/testify` for assertions in tests. Be mindful about the choice of `require` and `assert`, the former should be used when the test cannot proceed meaningfully after a failed assertion.
- Every 5xx response must log the error directly inside the handler, do not log errors in a shared helper.
## Localization
- Only edit `conf/locale/locale_en-US.ini`. The other `locale_*.ini` files are community-maintained translations. Do not add, remove, or rewrite keys in them, even when removing keys that are dead on the Go/template side.
## UI guidelines
- Design mobile-friendly. Every UI must look and work well on narrow viewports before adding desktop refinements via responsive breakpoints. Test at ~375px width before considering a UI done.
- Meet WCAG 2.2 AA at minimum. Specifically: every interactive control has a discernible accessible name (visible label or `aria-label`); color is never the sole carrier of information (pair with text, icon, or shape); text and meaningful icons meet 4.5:1 contrast against their background (3:1 for large text and UI components); focus is always visible and never trapped; touch targets are at least 24×24 CSS px (40×40 preferred). When unsure, lean toward more contrast, larger targets, and explicit labels.
- For work under `web/`, follow the patterns in [`web/DESIGN.md`](web/DESIGN.md) (typography, color hierarchy, surface chrome, file naming, accessibility specifics). Update that doc when a pattern is used in two places.
- When a page needs server data to render, fetch it in the TanStack Router route's `loader` so the page only mounts after the response arrives. Do not fire that fetch from a `useEffect` inside the page component, which causes a flash of empty UI before the data lands.
## Build instructions
- Prefer `task` command over vanilla `go` command when available. Use `--force` flag when necessary.
- Run `task lint` after every time you finish changing code, and fix all linter errors.
- Prefer `moon run <project>:<task>` over vanilla `go` or `pnpm` commands when available (e.g. `moon run gogs:build`, `moon run web:dev`). Pass `--force` to bypass cache when necessary.
- Run `moon run gogs:lint` after every time you finish changing Go code, and `moon run web:lint` after changing frontend code; fix all linter errors.
## Tool-use guidance
- Use `gh` CLI to access information on github.com that is not publicly available.
- Run the Chrome DevTools MCP in headless mode so it does not steal focus from the user's foreground browser session. After finishing any task that used the Chrome DevTools MCP, kill all `chrome-devtools-mcp` processes with `pkill -f chrome-devtools-mcp`.
## Source code control
- When pushing changes to a pull request from a fork, use SSH address and do not add remote.
- Never automatically executes commands that touches Git history even if the session does not require approvals, including but not limited to `rebase`, `commit`, `push`, `pull`, `reset`, `amend`. Exceptions are only allowed case-by-case.
- Do not amend commits unless being explicitly asked to do so.
- Never commit on the `main` branch directly unless being explicitly asked to do so. A single ask only grants a single commit action on the `main` branch.
- Never amend commits unless being explicitly asked to do so.
- When creating a git worktree, the worktree directory name must match its branch name. Do not use random or generated suffixes.
+35 -1
View File
@@ -4,7 +4,41 @@ All notable changes to Gogs are documented in this file.
## 0.15.0+dev (`main`)
## 0.14.0
### Changed
- Docker builds from `main` are now published only as `gogs/gogs:edge`, using the next-generation `Dockerfile.next`. The legacy `Dockerfile` no longer produces `main` builds. The `gogs/gogs:latest` and `gogs/gogs:next-latest` tags now always point to the highest published stable release, never to a back-patch on an older line. [#8278](https://github.com/gogs/gogs/pull/8278)
### Fixed
- _Security:_ Denial of service in repository and wiki file listing pages via crafted file names. [#8116](https://github.com/gogs/gogs/pull/8116) - [GHSA-3qq3-668m-v9mj](https://github.com/gogs/gogs/security/advisories/GHSA-3qq3-668m-v9mj)
- _Security:_ Reverse proxy authentication header was honored from any remote address, allowing user impersonation when Gogs was reachable directly. The header is now only trusted from addresses listed in `[auth] TRUSTED_PROXY_IPS`. [#8264](https://github.com/gogs/gogs/pull/8264) - [GHSA-w6j9-vw59-27wv](https://github.com/gogs/gogs/security/advisories/GHSA-w6j9-vw59-27wv)
- _Security:_ Server-side request forgery in webhook deliveries via HTTP redirects to local network addresses. [#8263](https://github.com/gogs/gogs/pull/8263) - [GHSA-c4v7-xg93-qf8g](https://github.com/gogs/gogs/security/advisories/GHSA-c4v7-xg93-qf8g)
- _Security:_ The "remember me" auto-login cookie was derived from database columns, so an attacker with a database dump could forge a valid cookie for any user. The auto-login cookie path has been removed entirely. Persistence is now provided by the server-issued session cookie. [#8289](https://github.com/gogs/gogs/pull/8289) - [GHSA-4pph-25p3-pw73](https://github.com/gogs/gogs/security/advisories/GHSA-4pph-25p3-pw73)
### Removed
- The `gogs cert` subcommand. [#8153](https://github.com/gogs/gogs/pull/8153)
- The `[email] DISABLE_HELO` configuration option. HELO/EHLO is now always sent during SMTP handshake. [#8164](https://github.com/gogs/gogs/pull/8164)
- Support for MSSQL as the database backend. Stay on 0.14 for continued usage. [#8173](https://github.com/gogs/gogs/pull/8173)
- Support for `memcache` as the cache adapter. Stay on 0.14 for continued usage. [#8270](https://github.com/gogs/gogs/pull/8270)
- The `/debug`, `/debug/pprof/*`, `/debug/profile/*`, and `/urlmap.json` endpoints. [#8271](https://github.com/gogs/gogs/pull/8271)
## 0.14.2
### Fixed
- _Security:_ Cross-repository LFS object overwrite via missing content hash verification. [#8166](https://github.com/gogs/gogs/pull/8166) - [GHSA-gmf8-978x-2fg2](https://github.com/gogs/gogs/security/advisories/GHSA-gmf8-978x-2fg2)
- _Security:_ Stored XSS via data URI in issue comments. [#8174](https://github.com/gogs/gogs/pull/8174) - [GHSA-xrcr-gmf5-2r8j](https://github.com/gogs/gogs/security/advisories/GHSA-xrcr-gmf5-2r8j)
- _Security:_ Release tag option injection in release deletion. [#8175](https://github.com/gogs/gogs/pull/8175) - [GHSA-v9vm-r24h-6rqm](https://github.com/gogs/gogs/security/advisories/GHSA-v9vm-r24h-6rqm)
- _Security:_ Stored XSS in branch and wiki views through author and committer names. [#8176](https://github.com/gogs/gogs/pull/8176) - [GHSA-vgvf-m4fw-938j](https://github.com/gogs/gogs/security/advisories/GHSA-vgvf-m4fw-938j)
- _Security:_ DOM-based XSS via issue meta selection on the issue page. [#8178](https://github.com/gogs/gogs/pull/8178) - [GHSA-vgjm-2cpf-4g7c](https://github.com/gogs/gogs/security/advisories/GHSA-vgjm-2cpf-4g7c)
- Unable to update files via web editor and API. [#8184](https://github.com/gogs/gogs/pull/8184)
### Removed
- Support for passing API access tokens via URL query parameters (`token`, `access_token`). Use the `Authorization` header instead. [#8177](https://github.com/gogs/gogs/pull/8177) - [GHSA-x9p5-w45c-7ffc](https://github.com/gogs/gogs/security/advisories/GHSA-x9p5-w45c-7ffc)
## 0.14.1
### Added
+17 -5
View File
@@ -1,4 +1,13 @@
FROM golang:alpine3.21 AS binarybuilder
FROM --platform=$BUILDPLATFORM node:24-alpine AS webbuilder
RUN corepack enable
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY web ./web
COPY conf/locale ./conf/locale
RUN pnpm install --frozen-lockfile
RUN pnpm --filter gogs-web run build
FROM golang:1.26-alpine3.23 AS binarybuilder
RUN apk --no-cache --no-progress add --virtual \
build-deps \
build-base \
@@ -7,11 +16,13 @@ RUN apk --no-cache --no-progress add --virtual \
WORKDIR /gogs.io/gogs
COPY . .
COPY --from=webbuilder /src/public/dist ./public/dist
RUN ./docker/build/install-task.sh
RUN TAGS="cert pam" task build
RUN go build -v -trimpath -tags "pam prod" \
-ldflags "-X 'gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')' -X 'gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)'" \
-o .bin/gogs ./cmd/gogs
FROM alpine:3.21
FROM alpine:3.23
RUN apk --no-cache --no-progress add \
bash \
ca-certificates \
@@ -23,7 +34,8 @@ RUN apk --no-cache --no-progress add \
shadow \
socat \
tzdata \
rsync
rsync \
"zlib>1.3.2"
ENV GOGS_CUSTOM=/data/gogs
+16 -4
View File
@@ -1,4 +1,13 @@
FROM golang:alpine3.23 AS binarybuilder
FROM --platform=$BUILDPLATFORM node:24-alpine AS webbuilder
RUN corepack enable
WORKDIR /src
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY web ./web
COPY conf/locale ./conf/locale
RUN pnpm install --frozen-lockfile
RUN pnpm --filter gogs-web run build
FROM golang:1.26-alpine3.23 AS binarybuilder
RUN apk --no-cache --no-progress add --virtual \
build-deps \
build-base \
@@ -7,9 +16,11 @@ RUN apk --no-cache --no-progress add --virtual \
WORKDIR /gogs.io/gogs
COPY . .
COPY --from=webbuilder /src/public/dist ./public/dist
RUN ./docker/build/install-task.sh
RUN TAGS="cert pam" task build
RUN go build -v -trimpath -tags "pam prod" \
-ldflags "-X 'gogs.io/gogs/internal/conf.BuildTime=$(date -u '+%Y-%m-%d %I:%M:%S %Z')' -X 'gogs.io/gogs/internal/conf.BuildCommit=$(git rev-parse HEAD)'" \
-o .bin/gogs ./cmd/gogs
FROM alpine:3.23
@@ -26,7 +37,8 @@ RUN apk --no-cache --no-progress add \
curl \
git \
linux-pam \
openssh-keygen
openssh-keygen \
"zlib>1.3.2"
ENV GOGS_CUSTOM=/data/gogs
+21 -17
View File
@@ -1,8 +1,19 @@
![gogs-brand](https://user-images.githubusercontent.com/2946214/146899259-6a8b58ad-8d6e-40d2-ab02-79dc6aadabbf.png)
[![GitHub Workflow Status](https://img.shields.io/github/checks-status/gogs/gogs/main?logo=github&style=for-the-badge)](https://github.com/gogs/gogs/actions?query=branch%3Amain) [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/gogs/gogs)
👉 Deploy on DigitalOcean and [get $200 in free credits](https://m.do.co/c/5aeb02268b55)!
<p>
<div align="center">
<img src="docs/images/logo-light.svg#gh-light-mode-only" alt="Gogs">
<img src="docs/images/logo-dark.svg#gh-dark-mode-only" alt="Gogs">
</div>
<div align="center">
<a href="https://github.com/gogs/gogs/actions?query=branch%3Amain"><img
src="https://img.shields.io/github/checks-status/gogs/gogs/main?logo=github&style=for-the-badge" alt="GitHub Workflow Status"></a>
<a href="https://sourcegraph.com/github.com/gogs/gogs"><img
src="https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph" alt="Sourcegraph"></a>
</div>
<div align="center">
👉 Deploy on DigitalOcean and <a href="https://m.do.co/c/5aeb02268b55">get $200 in free credits</a>!
</div>
</p>
## 🔮 Vision
@@ -13,10 +24,10 @@ The Gogs (`/gɑgz/`) project aims to build a simple, stable and extensible self-
- Please visit [our home page](https://gogs.io) for user documentation.
- Please refer to [CHANGELOG.md](CHANGELOG.md) for list of changes in each releases.
- Want to try it before doing anything else? Do it [online](https://try.gogs.io/gogs/gogs)!
- Having trouble? Help yourself with [troubleshooting](https://gogs.io/docs/intro/troubleshooting.html) or ask questions in [Discussions](https://github.com/gogs/gogs/discussions).
- Want to help with localization? Check out the [localization documentation](https://gogs.io/docs/features/i18n.html).
- Having trouble? Help yourself with [troubleshooting](https://gogs.io/asking/troubleshooting) or ask questions in [Discussions](https://github.com/gogs/gogs/discussions).
- Want to help with localization? Check out the [localization documentation](https://gogs.io/advancing/localization).
- Ready to get hands dirty? Read our [contributing guide](.github/CONTRIBUTING.md).
- Hmm... What about APIs? We have experimental support with [documentation](https://github.com/gogs/docs-api).
- Hmm... What about APIs? We have experimental support with [documentation](https://gogs.io/api-reference).
## 💌 Features
@@ -47,15 +58,7 @@ The Gogs (`/gɑgz/`) project aims to build a simple, stable and extensible self-
## 📜 Installation
Make sure you install the [prerequisites](https://gogs.io/docs/installation) first.
There are 6 ways to install Gogs:
- [Install from binary](https://gogs.io/docs/installation/install_from_binary.html)
- [Install from source](https://gogs.io/docs/installation/install_from_source.html)
- [Install from packages](https://gogs.io/docs/installation/install_from_packages.html)
- [Ship with Docker](https://github.com/gogs/gogs/tree/main/docker)
- [Try with Vagrant](https://github.com/geerlingguy/ansible-vagrant-examples/tree/master/gogs)
Please follow [the guide in our documentation](https://gogs.io/getting-started/installation).
### Deploy to cloud
@@ -94,7 +97,8 @@ There are 6 ways to install Gogs:
Other acknowledgments:
- Thanks [Egon Elbre](https://twitter.com/egonelbre) for designing the original version of the logo.
- Thanks [Crowdin](https://crowdin.com/project/gogs) for sponsoring open source translation plan.
- Thanks [Mintlify](https://mintlify.com) for sponsoring open source documentation plan.
- Thanks [Crowdin](https://crowdin.com) for sponsoring open source translation plan.
- Thanks [Buildkite](https://buildkite.com) for sponsoring open source CI/CD plan.
## 👋 Contributors
-102
View File
@@ -1,102 +0,0 @@
# Gogs
Gogs 是一款极易搭建的自助 Git 服务。
## 项目愿景
Gogs`/gɑgz/`)项目旨在打造一个以最简便的方式搭建简单、稳定和可扩展的自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发,并且支持 Go 语言支持的 **所有平台**,包括 Linux、macOS、Windows 和基于 ARM 的操作系统。
## 概览
- 请移步[官网](https://gogs.io)查看用户使用文档
- 请通过 [CHANGELOG.md](CHANGELOG.md) 文件查看各个版本的变更历史
- 想要先睹为快?直接去[在线体验](https://try.gogs.io/gogs/gogs)吧!
- 使用过程中遇到问题?尝试[故障排查](https://gogs.io/docs/intro/troubleshooting.html)或者前往[用户论坛](https://discuss.gogs.io/)获取帮助
- 希望帮助多国语言的翻译吗?请查看[本地化文档](https://gogs.io/docs/features/i18n.html)
- 准备搞点事情?请阅读[开发指南](docs/dev/local_development.md)配置开发环境
- 想调用 API 吗?请查看[文档](https://github.com/gogs/docs-api)吧
## 主要特性
- 控制面板、用户页面以及活动时间线
- 通过 SSH、HTTP 和 HTTPS 协议操作仓库
- 管理用户、组织和仓库
- 仓库和组织级 Webhook,包括 Slack、Discord 和钉钉
- 仓库 Git 钩子、部署密钥和 Git LFS
- 仓库工单(Issue)、合并请求(Pull Request)、Wiki、保护分支和多人协作
- 从其它代码平台迁移和镜像仓库以及 Wiki
- 在线编辑仓库文件和 Wiki
- Jupyter Notebook 和 PDF 的渲染
- 通过 SMTP、LDAP、反向代理、GitHub.com 和 GitHub 企业版进行用户认证
- 开启两步验证(2FA)登录
- 自定义 HTML 模板、静态文件和许多其它组件
- 多样的数据库后端,包括 PostgreSQL、MySQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb)
- 超过 [31 种语言](https://crowdin.com/project/gogs)的本地化
## 硬件要求
- 最低的系统硬件要求为一个廉价的树莓派
- 如果用于团队项目管理,建议使用 2 核 CPU 及 512MB 内存
- 当团队成员大量增加时,可以考虑添加 CPU 核数,内存占用保持不变
## 浏览器支持
- 请根据 [Semantic UI](https://github.com/Semantic-Org/Semantic-UI#browser-support) 查看具体支持的浏览器版本。
- 官方支持的最小 UI 尺寸为 **1024*768**,UI 不一定会在更小尺寸的设备上被破坏,但我们无法保证且不会修复。
## 安装部署
在安装 Gogs 之前,您需要先安装 [基本环境](https://gogs.io/docs/installation)。
然后,您可以通过以下 6 种方式来安装 Gogs:
- [二进制安装](https://gogs.io/docs/installation/install_from_binary.html)
- [源码安装](https://gogs.io/docs/installation/install_from_source.html)
- [包管理安装](https://gogs.io/docs/installation/install_from_packages.html)
- [采用 Docker 部署](https://github.com/gogs/gogs/tree/main/docker)
- [通过 Vagrant 安装](https://github.com/geerlingguy/ansible-vagrant-examples/tree/master/gogs)
- [通过基于 Kubernetes 的 Helm Charts](https://github.com/helm/charts/tree/master/incubator/gogs)
### 云端部署
- [OpenShift](https://github.com/tkisme/gogs-openshift)
- [Cloudron](https://cloudron.io/appstore.html#io.gogs.cloudronapp)
- [Scaleway](https://www.scaleway.com/imagehub/gogs/)
- [Sandstorm](https://github.com/cem/gogs-sandstorm)
- [sloppy.io](https://github.com/sloppyio/quickstarters/tree/master/gogs)
- [YunoHost](https://github.com/mbugeia/gogs_ynh)
- [DPlatform](https://github.com/j8r/DPlatform)
- [LunaNode](https://github.com/LunaNode/launchgogs)
### 使用教程
- [使用 Gogs 搭建自己的 Git 服务器](https://blog.mynook.info/post/host-your-own-git-server-using-gogs/)
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654)
## 软件、服务以及产品支持
- [Fabric8](http://fabric8.io/)DevOps
- [Jenkins](https://plugins.jenkins.io/gogs-webhook/)CI
- [Taiga](https://taiga.io/)(项目管理)
- [Puppet](https://forge.puppet.com/Siteminds/gogs)IT
- [Kanboard](https://github.com/kanboard/plugin-gogs-webhook)(项目管理)
- [BearyChat](https://bearychat.com/)(团队交流)
- [GitPitch](https://gitpitch.com/)Markdown 演示)
- [Synology](https://www.synology.com)Docker
- [Syncloud](https://syncloud.org/)(应用商店)
## 特别鸣谢
- 感谢 [Egon Elbre](https://twitter.com/egonelbre) 设计的 Logo。
- 感谢 [DigitalOcean](https://www.digitalocean.com) 和 [MonoVM](https://monovm.com) 提供服务器赞助。
- 感谢 [Crowdin](https://crowdin.com/project/gogs) 提供免费的开源项目本地化支持。
- 感谢 [Buildkite](https://buildkite.com) 提供免费的开源项目 CI/CD 支持。
## 贡献成员
- 您可以通过查看 [贡献者页面](https://github.com/gogs/gogs/graphs/contributors) 获取 TOP 100 的贡献者列表。
- 您可以通过查看 [TRANSLATORS](conf/locale/TRANSLATORS) 文件获取公开的翻译人员列表。
## 授权许可
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/gogs/gogs/blob/main/LICENSE) 文件中。
+2 -2
View File
@@ -2,9 +2,9 @@
## Supported versions
Only the latest minor version releases are supported (>= 0.13) for accepting vulnerability reports and patching fixes.
Only the latest minor version releases are supported (e.g., 0.14) for patching vulnerabilities. You can find the latest minor version in the [GitHub releases](https://github.com/gogs/gogs/releases) page.
Existing vulnerability reports are being tracked in [GitHub Security Advisories](https://github.com/gogs/gogs/security/advisories).
Existing vulnerability reports are being tracked in [GitHub Security Advisories](https://github.com/gogs/gogs/security/advisories). Not all accepted GHSA are published.
## Vulnerability lifecycle
-96
View File
@@ -1,96 +0,0 @@
version: '3'
vars:
BINARY_EXT:
sh: echo '{{if eq OS "windows"}}.exe{{end}}'
tasks:
default:
deps: [build]
web:
desc: Build the binary and start the web server
deps: [build]
env:
GOGS_WORK_DIR: '{{.ROOT_DIR}}'
cmds:
- .bin/gogs web
build:
desc: Build the binary
cmds:
- go build -v
-ldflags '
-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}"
-X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}"
'
-tags '{{.TAGS}}'
-trimpath -o .bin/gogs{{.BINARY_EXT}}
vars:
PKG_PATH: gogs.io/gogs/internal/conf
BUILD_TIME:
sh: date -u '+%Y-%m-%d %I:%M:%S %Z'
BUILD_COMMIT:
sh: git rev-parse HEAD
sources:
- go.mod
- gogs.go
- internal/**/*.go
- conf/**/*
- public/**/*
- templates/**/*
- custom/**/*
method: timestamp
generate-schemadoc:
desc: Generate database schema documentation
cmds:
- go generate ./internal/database/schemadoc
generate:
desc: Run all go:generate commands
cmds:
- go generate ./...
test:
desc: Run all tests.
cmds:
- go test -cover -race ./...
clean:
desc: Cleans up system meta files
cmds:
- find . -name "*.DS_Store" -type f -delete
less:
desc: Generate CSS from LESS files
cmds:
- lessc --clean-css --source-map "public/less/gogs.less" public/css/gogs.min.css
fixme:
desc: Show all occurrences of "FIXME"
cmds:
- grep -rnw "FIXME" internal
todo:
desc: Show all occurrences of "TODO"
cmds:
- grep -rnw "TODO" internal
legacy:
desc: Identify legacy and deprecated lines
cmds:
- grep -rnw "\(LEGACY\|Deprecated\)" internal
drop-test-db:
desc: Drop the test database
cmds:
- |
for dbname in $(psql -Xc "copy (select datname from pg_database where datname like 'gogs-%') to stdout"); do
dropdb "$dbname"
echo "dropped $dbname"
done
lint:
desc: Run all linters
cmds:
- golangci-lint run
+25 -25
View File
@@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@@ -7,27 +7,27 @@ import (
"runtime"
"github.com/cockroachdb/errors"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
)
var (
Admin = cli.Command{
adminCommand = cli.Command{
Name: "admin",
Usage: "Perform admin operations on command line",
Description: `Allow using internal logic of Gogs without hacking into the source code
to make automatic initialization process more smoothly`,
Subcommands: []cli.Command{
subcmdCreateUser,
subcmdDeleteInactivateUsers,
subcmdDeleteRepositoryArchives,
subcmdDeleteMissingRepositories,
subcmdGitGcRepos,
subcmdRewriteAuthorizedKeys,
subcmdSyncRepositoryHooks,
subcmdReinitMissingRepositories,
Commands: []*cli.Command{
&subcmdCreateUser,
&subcmdDeleteInactivateUsers,
&subcmdDeleteRepositoryArchives,
&subcmdDeleteMissingRepositories,
&subcmdGitGcRepos,
&subcmdRewriteAuthorizedKeys,
&subcmdSyncRepositoryHooks,
&subcmdReinitMissingRepositories,
},
}
@@ -129,16 +129,16 @@ to make automatic initialization process more smoothly`,
}
)
func runCreateUser(c *cli.Context) error {
if !c.IsSet("name") {
func runCreateUser(ctx context.Context, cmd *cli.Command) error {
if !cmd.IsSet("name") {
return errors.New("Username is not specified")
} else if !c.IsSet("password") {
} else if !cmd.IsSet("password") {
return errors.New("Password is not specified")
} else if !c.IsSet("email") {
} else if !cmd.IsSet("email") {
return errors.New("Email is not specified")
}
err := conf.Init(c.String("config"))
err := conf.Init(configFromLineage(cmd))
if err != nil {
return errors.Wrap(err, "init configuration")
}
@@ -149,13 +149,13 @@ func runCreateUser(c *cli.Context) error {
}
user, err := database.Handle.Users().Create(
context.Background(),
c.String("name"),
c.String("email"),
ctx,
cmd.String("name"),
cmd.String("email"),
database.CreateUserOptions{
Password: c.String("password"),
Password: cmd.String("password"),
Activated: true,
Admin: c.Bool("admin"),
Admin: cmd.Bool("admin"),
},
)
if err != nil {
@@ -166,9 +166,9 @@ func runCreateUser(c *cli.Context) error {
return nil
}
func adminDashboardOperation(operation func() error, successMessage string) func(*cli.Context) error {
return func(c *cli.Context) error {
err := conf.Init(c.String("config"))
func adminDashboardOperation(operation func() error, successMessage string) func(context.Context, *cli.Command) error {
return func(_ context.Context, cmd *cli.Command) error {
err := conf.Init(configFromLineage(cmd))
if err != nil {
return errors.Wrap(err, "init configuration")
}
+15 -15
View File
@@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@@ -11,16 +11,16 @@ import (
"github.com/cockroachdb/errors"
"github.com/unknwon/cae/zip"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
"gopkg.in/ini.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/osx"
)
var Backup = cli.Command{
var backupCommand = cli.Command{
Name: "backup",
Usage: "Backup files and database",
Description: `Backup dumps and compresses all related files and database into zip file,
@@ -44,10 +44,10 @@ const (
archiveRootDir = "gogs-backup"
)
func runBackup(c *cli.Context) error {
zip.Verbose = c.Bool("verbose")
func runBackup(ctx context.Context, cmd *cli.Command) error {
zip.Verbose = cmd.Bool("verbose")
err := conf.Init(c.String("config"))
err := conf.Init(configFromLineage(cmd))
if err != nil {
return errors.Wrap(err, "init configuration")
}
@@ -58,8 +58,8 @@ func runBackup(c *cli.Context) error {
return errors.Wrap(err, "set engine")
}
tmpDir := c.String("tempdir")
if !osutil.Exist(tmpDir) {
tmpDir := cmd.String("tempdir")
if !osx.Exist(tmpDir) {
log.Fatal("'--tempdir' does not exist: %s", tmpDir)
}
rootDir, err := os.MkdirTemp(tmpDir, "gogs-backup-")
@@ -78,7 +78,7 @@ func runBackup(c *cli.Context) error {
log.Fatal("Failed to save metadata '%s': %v", metaFile, err)
}
archiveName := filepath.Join(c.String("target"), c.String("archive-name"))
archiveName := filepath.Join(cmd.String("target"), cmd.String("archive-name"))
log.Info("Packing backup files to: %s", archiveName)
z, err := zip.Create(archiveName)
@@ -91,14 +91,14 @@ func runBackup(c *cli.Context) error {
// Database
dbDir := filepath.Join(rootDir, "db")
if err = database.DumpDatabase(context.Background(), conn, dbDir, c.Bool("verbose")); err != nil {
if err = database.DumpDatabase(ctx, conn, dbDir, cmd.Bool("verbose")); err != nil {
log.Fatal("Failed to dump database: %v", err)
}
if err = z.AddDir(archiveRootDir+"/db", dbDir); err != nil {
log.Fatal("Failed to include 'db': %v", err)
}
if !c.Bool("database-only") {
if !cmd.Bool("database-only") {
// Custom files
err = addCustomDirToBackup(z)
if err != nil {
@@ -108,7 +108,7 @@ func runBackup(c *cli.Context) error {
// Data files
for _, dir := range []string{"ssh", "attachments", "avatars", "repo-avatars"} {
dirPath := filepath.Join(conf.Server.AppDataPath, dir)
if !osutil.IsDir(dirPath) {
if !osx.IsDir(dirPath) {
continue
}
@@ -119,10 +119,10 @@ func runBackup(c *cli.Context) error {
}
// Repositories
if !c.Bool("exclude-repos") && !c.Bool("database-only") {
if !cmd.Bool("exclude-repos") && !cmd.Bool("database-only") {
reposDump := filepath.Join(rootDir, "repositories.zip")
log.Info("Dumping repositories in %q", conf.Repository.Root)
if c.Bool("exclude-mirror-repos") {
if cmd.Bool("exclude-mirror-repos") {
repos, err := database.GetNonMirrorRepositories()
if err != nil {
log.Fatal("Failed to get non-mirror repositories: %v", err)
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"strings"
"github.com/urfave/cli/v3"
)
func stringFlag(name, value, usage string) *cli.StringFlag {
parts := strings.SplitN(name, ", ", 2)
f := &cli.StringFlag{
Name: parts[0],
Value: value,
Usage: usage,
}
if len(parts) > 1 {
f.Aliases = []string{parts[1]}
}
return f
}
// configFromLineage walks the command lineage to find the --config flag value.
// This is needed because subcommands may not directly see flags set on parent commands.
func configFromLineage(cmd *cli.Command) string {
for _, c := range cmd.Lineage() {
if c.IsSet("config") {
return c.String("config")
}
}
return ""
}
func intFlag(name string, value int, usage string) *cli.IntFlag {
parts := strings.SplitN(name, ", ", 2)
f := &cli.IntFlag{
Name: parts[0],
Value: value,
Usage: usage,
}
if len(parts) > 1 {
f.Aliases = []string{parts[1]}
}
return f
}
func boolFlag(name, usage string) *cli.BoolFlag {
parts := strings.SplitN(name, ", ", 2)
f := &cli.BoolFlag{
Name: parts[0],
Usage: usage,
}
if len(parts) > 1 {
f.Aliases = []string{parts[1]}
}
return f
}
+24 -21
View File
@@ -1,18 +1,19 @@
package cmd
package main
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/unknwon/com"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
log "unknwon.dev/clog/v2"
"github.com/gogs/git-module"
@@ -21,20 +22,21 @@ import (
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/email"
"gogs.io/gogs/internal/httplib"
"gogs.io/gogs/internal/osx"
)
var (
Hook = cli.Command{
hookCommand = cli.Command{
Name: "hook",
Usage: "Delegate commands to corresponding Git hooks",
Description: "All sub-commands should only be called by Git",
Flags: []cli.Flag{
stringFlag("config, c", "", "Custom configuration file path"),
},
Subcommands: []cli.Command{
subcmdHookPreReceive,
subcmdHookUpadte,
subcmdHookPostReceive,
Commands: []*cli.Command{
&subcmdHookPreReceive,
&subcmdHookUpadte,
&subcmdHookPostReceive,
},
}
@@ -58,11 +60,11 @@ var (
}
)
func runHookPreReceive(c *cli.Context) error {
func runHookPreReceive(_ context.Context, cmd *cli.Command) error {
if os.Getenv("SSH_ORIGINAL_COMMAND") == "" {
return nil
}
setup(c, "pre-receive.log", true)
setup(cmd, "pre-receive.log", true)
isWiki := strings.Contains(os.Getenv(database.EnvRepoCustomHooksPath), ".wiki.git/")
@@ -85,7 +87,7 @@ func runHookPreReceive(c *cli.Context) error {
branchName := git.RefShortName(string(fields[2]))
// Branch protection
repoID := com.StrTo(os.Getenv(database.EnvRepoID)).MustInt64()
repoID, _ := strconv.ParseInt(os.Getenv(database.EnvRepoID), 10, 64)
protectBranch, err := database.GetProtectBranchOfRepoByName(repoID, branchName)
if err != nil {
if database.IsErrBranchNotExist(err) {
@@ -101,7 +103,7 @@ func runHookPreReceive(c *cli.Context) error {
bypassRequirePullRequest := false
// Check if user is in whitelist when enabled
userID := com.StrTo(os.Getenv(database.EnvAuthUserID)).MustInt64()
userID, _ := strconv.ParseInt(os.Getenv(database.EnvAuthUserID), 10, 64)
if protectBranch.EnableWhitelist {
if !database.IsUserInProtectBranchWhitelist(repoID, userID, branchName) {
fail(fmt.Sprintf("Branch '%s' is protected and you are not in the push whitelist", branchName), "")
@@ -131,7 +133,7 @@ func runHookPreReceive(c *cli.Context) error {
}
customHooksPath := filepath.Join(os.Getenv(database.EnvRepoCustomHooksPath), "pre-receive")
if !com.IsFile(customHooksPath) {
if !osx.IsFile(customHooksPath) {
return nil
}
@@ -151,13 +153,13 @@ func runHookPreReceive(c *cli.Context) error {
return nil
}
func runHookUpdate(c *cli.Context) error {
func runHookUpdate(_ context.Context, cmd *cli.Command) error {
if os.Getenv("SSH_ORIGINAL_COMMAND") == "" {
return nil
}
setup(c, "update.log", false)
setup(cmd, "update.log", false)
args := c.Args()
args := cmd.Args().Slice()
if len(args) != 3 {
fail("Arguments received are not equal to three", "Arguments received are not equal to three")
} else if args[0] == "" {
@@ -165,7 +167,7 @@ func runHookUpdate(c *cli.Context) error {
}
customHooksPath := filepath.Join(os.Getenv(database.EnvRepoCustomHooksPath), "update")
if !com.IsFile(customHooksPath) {
if !osx.IsFile(customHooksPath) {
return nil
}
@@ -185,11 +187,11 @@ func runHookUpdate(c *cli.Context) error {
return nil
}
func runHookPostReceive(c *cli.Context) error {
func runHookPostReceive(_ context.Context, cmd *cli.Command) error {
if os.Getenv("SSH_ORIGINAL_COMMAND") == "" {
return nil
}
setup(c, "post-receive.log", true)
setup(cmd, "post-receive.log", true)
// Post-receive hook does more than just gather Git information,
// so we need to setup additional services for email notifications.
@@ -213,11 +215,12 @@ func runHookPostReceive(c *cli.Context) error {
continue
}
pusherID, _ := strconv.ParseInt(os.Getenv(database.EnvAuthUserID), 10, 64)
options := database.PushUpdateOptions{
OldCommitID: string(fields[0]),
NewCommitID: string(fields[1]),
FullRefspec: string(fields[2]),
PusherID: com.StrTo(os.Getenv(database.EnvAuthUserID)).MustInt64(),
PusherID: pusherID,
PusherName: os.Getenv(database.EnvAuthUserName),
RepoUserName: os.Getenv(database.EnvRepoOwnerName),
RepoName: os.Getenv(database.EnvRepoName),
@@ -249,7 +252,7 @@ func runHookPostReceive(c *cli.Context) error {
}
customHooksPath := filepath.Join(os.Getenv(database.EnvRepoCustomHooksPath), "post-receive")
if !com.IsFile(customHooksPath) {
if !osx.IsFile(customHooksPath) {
return nil
}
+18 -17
View File
@@ -1,28 +1,29 @@
package cmd
package main
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/cockroachdb/errors"
"github.com/unknwon/com"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/osx"
)
var (
Import = cli.Command{
importCommand = cli.Command{
Name: "import",
Usage: "Import portable data as local Gogs data",
Description: `Allow user import data from other Gogs installations to local instance
without manually hacking the data files`,
Subcommands: []cli.Command{
subcmdImportLocale,
Commands: []*cli.Command{
&subcmdImportLocale,
},
}
@@ -38,19 +39,19 @@ without manually hacking the data files`,
}
)
func runImportLocale(c *cli.Context) error {
if !c.IsSet("source") {
func runImportLocale(_ context.Context, cmd *cli.Command) error {
if !cmd.IsSet("source") {
return errors.New("source directory is not specified")
} else if !c.IsSet("target") {
} else if !cmd.IsSet("target") {
return errors.New("target directory is not specified")
}
if !com.IsDir(c.String("source")) {
return errors.Newf("source directory %q does not exist or is not a directory", c.String("source"))
} else if !com.IsDir(c.String("target")) {
return errors.Newf("target directory %q does not exist or is not a directory", c.String("target"))
if !osx.IsDir(cmd.String("source")) {
return errors.Newf("source directory %q does not exist or is not a directory", cmd.String("source"))
} else if !osx.IsDir(cmd.String("target")) {
return errors.Newf("target directory %q does not exist or is not a directory", cmd.String("target"))
}
err := conf.Init(c.String("config"))
err := conf.Init(configFromLineage(cmd))
if err != nil {
return errors.Wrap(err, "init configuration")
}
@@ -64,9 +65,9 @@ func runImportLocale(c *cli.Context) error {
// Cut out en-US.
for _, lang := range conf.I18n.Langs[1:] {
name := fmt.Sprintf("locale_%s.ini", lang)
source := filepath.Join(c.String("source"), name)
target := filepath.Join(c.String("target"), name)
if !com.IsFile(source) {
source := filepath.Join(cmd.String("source"), name)
target := filepath.Join(cmd.String("target"), name)
if !osx.IsFile(source) {
continue
}
+89
View File
@@ -0,0 +1,89 @@
package web
import (
"crypto/tls"
"strconv"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/flamego/cache"
"github.com/flamego/cache/redis"
"gopkg.in/ini.v1"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/strx"
)
func parseCacheOptions(confOpts conf.CacheOptions) (cache.Options, error) {
opts := cache.Options{
GCInterval: time.Duration(confOpts.Interval) * time.Second,
}
switch strx.Coalesce(strings.ToLower(confOpts.Adapter), "memory") {
case "memory":
opts.Initer = cache.MemoryIniter()
case "file":
opts.Initer = cache.FileIniter()
opts.Config = cache.FileConfig{RootDir: confOpts.Host}
case "redis":
cfg, err := parseRedisConfig(confOpts.Host)
if err != nil {
return cache.Options{}, errors.Wrap(err, "parse redis config")
}
opts.Initer = redis.Initer()
opts.Config = cfg
default:
return cache.Options{}, errors.Errorf("unsupported adapter %q", confOpts.Adapter)
}
return opts, nil
}
func parseRedisConfig(host string) (redis.Config, error) {
cfg, err := ini.Load([]byte(strings.ReplaceAll(host, ",", "\n")))
if err != nil {
return redis.Config{}, errors.Wrap(err, "load HOST")
}
var config redis.Config
for k, v := range cfg.Section("").KeysHash() {
switch k {
case "network":
config.Options.Network = v
case "addr":
config.Options.Addr = v
case "password":
config.Options.Password = v
case "db":
n, err := strconv.Atoi(v)
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse db %q", v)
}
config.Options.DB = n
case "pool_size":
n, err := strconv.Atoi(v)
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse pool_size %q", v)
}
config.Options.PoolSize = n
case "idle_timeout":
d, err := time.ParseDuration(v + "s")
if err != nil {
return redis.Config{}, errors.Wrapf(err, "parse idle_timeout %q", v)
}
config.Options.ConnMaxIdleTime = d
case "prefix":
config.KeyPrefix = v
case "tls":
// Matches go-macaron/session/redis: any non-empty `tls=` value enables
// TLS with InsecureSkipVerify.
config.Options.TLSConfig = &tls.Config{InsecureSkipVerify: true}
case "hset_name":
// Macaron stored values in a single Redis hash named by this key,
// whereas Flamego stores per-key with KeyPrefix, so this knob has no equivalent.
default:
return redis.Config{}, errors.Errorf("unsupported redis HOST key %q", k)
}
}
return config, nil
}
+88
View File
@@ -0,0 +1,88 @@
package web
import (
"net/http"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/ptrx"
)
// repoContext is the request-scoped viewer of the repository. Viewer can be an
// authenticated or anonymous user.
type repoContext struct {
Owner *database.User
Repo *database.Repository
ViewerID int64
viewerAccess database.AccessMode
}
func (c *repoContext) ViewerCanRead() bool {
return c.viewerAccess >= database.AccessModeRead
}
func (c *repoContext) ViewerCanWrite() bool {
return c.viewerAccess >= database.AccessModeWrite
}
func (c *repoContext) ViewerCanAdminister() bool {
return c.viewerAccess >= database.AccessModeAdmin
}
// withRepoContext injects the repoContext of the repository derived from the
// route.
func withRepoContext(c flamego.Context, user *database.User) {
ctx := c.Request().Context()
w := c.ResponseWriter()
ownerName := c.Param("owner")
repoName := c.Param("repo")
owner, err := database.Handle.Users().GetByUsername(ctx, ownerName)
if err != nil {
if database.IsErrUserNotExist(err) {
writeErrorResponse(w, http.StatusNotFound, errors.New("repository does not exist"))
return
}
log.Error("repoContext: get user by username %q: %v", ownerName, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "get owner"))
return
}
repo, err := database.Handle.Repositories().GetByName(ctx, owner.ID, repoName)
if err != nil {
if database.IsErrRepoNotExist(err) {
writeErrorResponse(w, http.StatusNotFound, errors.New("repository does not exist"))
return
}
log.Error("repoContext: get repo by name %q/%q: %v", ownerName, repoName, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "get repo"))
return
}
viewer := ptrx.Deref(user, database.User{})
var viewerAccess database.AccessMode
if viewer.IsAdmin {
viewerAccess = database.AccessModeOwner
} else {
viewerAccess = database.Handle.Permissions().AccessMode(
ctx,
viewer.ID,
repo.ID,
database.AccessModeOptions{
OwnerID: owner.ID,
Private: repo.IsPrivate,
},
)
}
c.Map(&repoContext{
Owner: owner,
Repo: repo,
ViewerID: viewer.ID,
viewerAccess: viewerAccess,
})
}
@@ -1,7 +1,8 @@
package cmd
package web
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
@@ -9,19 +10,20 @@ import (
"net/http/fcgi"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/flamego/cache"
"github.com/flamego/captcha"
"github.com/flamego/flamego"
"github.com/go-macaron/binding"
"github.com/go-macaron/cache"
"github.com/go-macaron/captcha"
macaroncache "github.com/go-macaron/cache"
"github.com/go-macaron/csrf"
"github.com/go-macaron/gzip"
"github.com/go-macaron/i18n"
"github.com/go-macaron/session"
"github.com/go-macaron/toolbox"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/unknwon/com"
"github.com/urfave/cli"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
@@ -31,145 +33,39 @@ import (
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/form"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/osx"
"gogs.io/gogs/internal/route"
"gogs.io/gogs/internal/route/admin"
apiv1 "gogs.io/gogs/internal/route/api/v1"
"gogs.io/gogs/internal/route/dev"
"gogs.io/gogs/internal/route/lfs"
"gogs.io/gogs/internal/route/org"
"gogs.io/gogs/internal/route/repo"
"gogs.io/gogs/internal/route/user"
"gogs.io/gogs/internal/template"
"gogs.io/gogs/internal/urlx"
"gogs.io/gogs/public"
"gogs.io/gogs/templates"
)
var Web = cli.Command{
Name: "web",
Usage: "Start web server",
Description: `Gogs web server is the only thing you need to run,
and it takes care of all the other things for you`,
Action: runWeb,
Flags: []cli.Flag{
stringFlag("port, p", "3000", "Temporary port number to prevent conflict"),
stringFlag("config, c", "", "Custom configuration file path"),
},
}
// newMacaron initializes Macaron instance.
func newMacaron() *macaron.Macaron {
m := macaron.New()
if !conf.Server.DisableRouterLog {
m.Use(macaron.Logger())
}
m.Use(macaron.Recovery())
if conf.Server.EnableGzip {
m.Use(gzip.Gziper())
}
if conf.Server.Protocol == "fcgi" {
m.SetURLPrefix(conf.Server.Subpath)
}
// Register custom middleware first to make it possible to override files under "public".
m.Use(macaron.Static(
filepath.Join(conf.CustomDir(), "public"),
macaron.StaticOptions{
SkipLogging: conf.Server.DisableRouterLog,
},
))
var publicFs http.FileSystem
if !conf.Server.LoadAssetsFromDisk {
publicFs = http.FS(public.Files)
}
m.Use(macaron.Static(
filepath.Join(conf.WorkDir(), "public"),
macaron.StaticOptions{
ETag: true,
SkipLogging: conf.Server.DisableRouterLog,
FileSystem: publicFs,
},
))
m.Use(macaron.Static(
conf.Picture.AvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: conf.UsersAvatarPathPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
m.Use(macaron.Static(
conf.Picture.RepositoryAvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: database.RepoAvatarURLPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
customDir := filepath.Join(conf.CustomDir(), "templates")
renderOpt := macaron.RenderOptions{
Directory: filepath.Join(conf.WorkDir(), "templates"),
AppendDirectories: []string{customDir},
Funcs: template.FuncMap(),
IndentJSON: macaron.Env != macaron.PROD,
}
if !conf.Server.LoadAssetsFromDisk {
renderOpt.TemplateFileSystem = templates.NewTemplateFileSystem("", customDir)
}
m.Use(macaron.Renderer(renderOpt))
localeNames, err := embedConf.FileNames("locale")
// Run starts the web server with the given configuration path and port override.
func Run(configPath string, portOverride int) error {
err := route.GlobalInit(configPath)
if err != nil {
log.Fatal("Failed to list locale files: %v", err)
return errors.Wrap(err, "initialize application")
}
localeFiles := make(map[string][]byte)
for _, name := range localeNames {
localeFiles[name], err = embedConf.Files.ReadFile("locale/" + name)
if err != nil {
log.Fatal("Failed to read locale file %q: %v", name, err)
}
}
m.Use(i18n.I18n(i18n.Options{
SubURL: conf.Server.Subpath,
Files: localeFiles,
CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
Langs: conf.I18n.Langs,
Names: conf.I18n.Names,
DefaultLang: "en-US",
Redirect: true,
}))
m.Use(cache.Cacher(cache.Options{
Adapter: conf.Cache.Adapter,
AdapterConfig: conf.Cache.Host,
Interval: conf.Cache.Interval,
}))
m.Use(captcha.Captchaer(captcha.Options{
SubURL: conf.Server.Subpath,
}))
m.Use(toolbox.Toolboxer(m, toolbox.Options{
HealthCheckFuncs: []*toolbox.HealthCheckFuncDesc{
{
Desc: "Database connection",
Func: database.Ping,
},
},
}))
return m
}
func runWeb(c *cli.Context) error {
err := route.GlobalInit(c.String("config"))
m, err := newMacaron()
if err != nil {
log.Fatal("Failed to initialize application: %v", err)
return errors.Wrap(err, "initialize macaron")
}
m := newMacaron()
webHandler, err := newRoutingHandler()
if err != nil {
return errors.Wrap(err, "initialize web handler")
}
reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: conf.Auth.RequireSigninView})
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
bindIgnErr := binding.BindIgnErr
@@ -190,20 +86,6 @@ func runWeb(c *cli.Context) error {
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
// ***** START: User *****
m.Group("/user", func() {
m.Group("/login", func() {
m.Combo("").Get(user.Login).
Post(bindIgnErr(form.SignIn{}), user.LoginPost)
m.Combo("/two_factor").Get(user.LoginTwoFactor).Post(user.LoginTwoFactorPost)
m.Combo("/two_factor_recovery_code").Get(user.LoginTwoFactorRecoveryCode).Post(user.LoginTwoFactorRecoveryCodePost)
})
m.Get("/sign_up", user.SignUp)
m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
m.Get("/reset_password", user.ResetPasswd)
m.Post("/reset_password", user.ResetPasswdPost)
}, reqSignOut)
m.Group("/user/settings", func() {
m.Get("", user.Settings)
m.Post("", bindIgnErr(form.UpdateProfile{}), user.SettingsPost)
@@ -245,12 +127,8 @@ func runWeb(c *cli.Context) error {
})
m.Group("/user", func() {
m.Any("/activate", user.Activate)
m.Any("/activate_email", user.ActivateEmail)
m.Get("/email2user", user.Email2User)
m.Get("/forget_password", user.ForgotPasswd)
m.Post("/forget_password", user.ForgotPasswdPost)
m.Post("/logout", user.SignOut)
})
// ***** END: User *****
@@ -308,7 +186,7 @@ func runWeb(c *cli.Context) error {
if err != nil {
c.NotFoundOrError(err, "get attachment by UUID")
return
} else if !com.IsFile(attach.LocalPath()) {
} else if !osx.IsFile(attach.LocalPath()) {
c.NotFound()
return
}
@@ -340,10 +218,6 @@ func runWeb(c *cli.Context) error {
m.Post("/action/:action", user.Action)
}, reqSignIn, context.InjectParamsUser())
if macaron.Env == macaron.DEV {
m.Get("/template/*", dev.TemplatePreview)
}
reqRepoAdmin := context.RequireRepoAdmin()
reqRepoWriter := context.RequireRepoWriter()
@@ -538,10 +412,9 @@ func runWeb(c *cli.Context) error {
c.Data["PageIsViewFiles"] = true
})
// FIXME: Should use c.Repo.PullRequest to unify template, currently we have inconsistent URL
// for PR in same repository. After select branch on the page, the URL contains redundant head user name.
// e.g. /org1/test-repo/compare/master...org1:develop
// which should be /org1/test-repo/compare/master...develop
// FIXME: Should use c.Repo.PullRequest to unify the template. Same-repo PR URLs include a
// redundant head user, e.g. /org1/test-repo/compare/master...org1:develop should be
// /org1/test-repo/compare/master...develop.
m.Combo("/compare/*", repo.MustAllowPulls).Get(repo.CompareAndPullRequest).
Post(bindIgnErr(form.NewIssue{}), repo.CompareAndPullRequestPost)
@@ -610,12 +483,14 @@ func runWeb(c *cli.Context) error {
m.Group("", func() {
m.Get("/src/*", repo.Home)
m.Get("/raw/*", repo.SingleDownload)
m.Get("/commits/*", repo.RefCommits)
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.Diff)
m.Get("/forks", repo.Forks)
}, repo.MustBeNotBare, context.RepoRef())
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.MustBeNotBare, repo.RawDiff)
// Bridged to Flamego to skip the legacy `RepoRef` middleware, which double-resolves the ref.
m.Get("/raw/*", flamegoBridger(webHandler))
m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", flamegoBridger(webHandler))
// Constrain SHA shape so non-matching `/commit/...` paths 404 instead of loading the SPA with a bad param.
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.MustBeNotBare, func(c *context.Context) { c.ServeWeb() })
m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.MustBeNotBare, context.RepoRef(), repo.CompareDiff)
}, ignSignIn, context.RepoAssignment())
@@ -634,6 +509,11 @@ func runWeb(c *cli.Context) error {
m.Group("/api", func() {
apiv1.RegisterRoutes(m)
}, ignSignIn)
m.Any("/api/web/*", flamegoBridger(webHandler))
m.Get("/redirect", flamegoBridger(webHandler))
m.Get("/captcha/*", flamegoBridger(webHandler))
m.Any("/*", func(c *context.Context) { c.ServeWeb() })
},
session.Sessioner(session.Options{
Provider: conf.Session.Provider,
@@ -643,6 +523,7 @@ func runWeb(c *cli.Context) error {
Gclifetime: conf.Session.GCInterval,
Maxlifetime: conf.Session.MaxLifeTime,
Secure: conf.Session.CookieSecure,
CookieLifeTime: 86400 * conf.Security.LoginRememberDays,
}),
csrf.Csrfer(csrf.Options{
Secret: conf.Security.SecretKey,
@@ -654,7 +535,7 @@ func runWeb(c *cli.Context) error {
SetCookie: true,
Secure: conf.Server.URL.Scheme == "https",
}),
context.Contexter(context.NewStore()),
context.Contexter(context.NewStore(), webHandler),
)
// ***************************
@@ -668,7 +549,18 @@ func runWeb(c *cli.Context) error {
lfs.RegisterRoutes(m.Router)
})
m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP)
gitHTTP := []macaron.Handler{context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP}
m.Route("/info/refs", "GET,OPTIONS", gitHTTP...)
m.Route("/HEAD", "GET,OPTIONS", gitHTTP...)
m.Route("/git-upload-pack", "POST,OPTIONS", gitHTTP...)
m.Route("/git-receive-pack", "POST,OPTIONS", gitHTTP...)
m.Route("/objects/info/alternates", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/http-alternates", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/packs", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/info/*", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/:prefix([0-9a-f]{2})/:suffix([0-9a-f]{38})", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/pack/pack-:sha([0-9a-f]{40}).pack", "GET,OPTIONS", gitHTTP...)
m.Route("/objects/pack/pack-:sha([0-9a-f]{40}).idx", "GET,OPTIONS", gitHTTP...)
})
// ***************************
@@ -695,20 +587,19 @@ func runWeb(c *cli.Context) error {
}
})
m.NotFound(route.NotFound)
// Flag for port number in case first time run conflict.
if c.IsSet("port") {
conf.Server.URL.Host = strings.Replace(conf.Server.URL.Host, ":"+conf.Server.URL.Port(), ":"+c.String("port"), 1)
if portOverride > 0 {
port := strconv.Itoa(portOverride)
conf.Server.URL.Host = strings.Replace(conf.Server.URL.Host, ":"+conf.Server.URL.Port(), ":"+port, 1)
conf.Server.ExternalURL = conf.Server.URL.String()
conf.Server.HTTPPort = c.String("port")
conf.Server.HTTPPort = portOverride
}
var listenAddr string
if conf.Server.Protocol == "unix" {
listenAddr = conf.Server.HTTPAddr
} else {
listenAddr = fmt.Sprintf("%s:%s", conf.Server.HTTPAddr, conf.Server.HTTPPort)
listenAddr = fmt.Sprintf("%s:%d", conf.Server.HTTPAddr, conf.Server.HTTPPort)
}
log.Info("Available on %s", conf.Server.ExternalURL)
@@ -750,33 +641,229 @@ func runWeb(c *cli.Context) error {
err = fcgi.Serve(nil, m)
case "unix":
if osutil.Exist(listenAddr) {
if osx.Exist(listenAddr) {
err = os.Remove(listenAddr)
if err != nil {
log.Fatal("Failed to remove existing Unix domain socket: %v", err)
return errors.Wrap(err, "remove existing Unix domain socket")
}
}
var listener *net.UnixListener
listener, err = net.ListenUnix("unix", &net.UnixAddr{Name: listenAddr, Net: "unix"})
if err != nil {
log.Fatal("Failed to listen on Unix networks: %v", err)
return errors.Wrap(err, "listen on Unix network")
}
// FIXME: add proper implementation of signal capture on all protocols
// execute this on SIGTERM or SIGINT: listener.Close()
if err = os.Chmod(listenAddr, conf.Server.UnixSocketMode); err != nil {
log.Fatal("Failed to change permission of Unix domain socket: %v", err)
return errors.Wrap(err, "change permission of Unix domain socket")
}
err = http.Serve(listener, m)
default:
log.Fatal("Unexpected server protocol: %s", conf.Server.Protocol)
return errors.Newf("unexpected server protocol: %s", conf.Server.Protocol)
}
if err != nil {
log.Fatal("Failed to start server: %v", err)
return errors.Wrap(err, "start server")
}
return nil
}
func newRoutingHandler() (http.Handler, error) {
f := flamego.New()
f.Use(flamego.Recovery())
f.Use(flamegoInjector)
f.Use(captcha.Captchaer(captcha.Options{URLPrefix: "/captcha/"}))
cacherOpts, err := parseCacheOptions(conf.Cache)
if err != nil {
return nil, errors.Wrap(err, "parse cache options")
}
f.Use(cache.Cacher(cacherOpts))
f.ReturnHandler(func(c flamego.Context, statusCode int, resp any, err error) {
w := c.ResponseWriter()
w.Header().Set("Cache-Control", "no-store")
if err != nil {
msg := err.Error()
if statusCode >= http.StatusInternalServerError && conf.IsProdMode() {
msg = "Internal server error"
}
resp = map[string]any{"error": msg}
}
if resp == nil {
w.WriteHeader(statusCode)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(resp)
})
f.Get("/redirect", getRedirect)
// The captcha middleware writes the response. This route exists so the request reaches it.
f.Get("/captcha/image.jpeg", func() {})
f.Group("/{owner}/{repo}", func() {
f.Get("/commit/{sha: /[0-9a-f]{7,40}/}.{format: /(diff|patch)/}", getRepoCommitRaw)
f.Get("/raw/{ref}/{filepath: **}", getRepoRawFile)
}, withRepoContext)
mountWebAPIRoutes(f)
err = mountWebAppRoutes(f)
if err != nil {
return nil, errors.Wrap(err, "mount web app routes")
}
return f, nil
}
func getRedirect(c flamego.Context) {
to := c.Request().URL.Query().Get("to")
if !urlx.IsSameSite(to) {
to = conf.Server.Subpath + "/"
}
c.Redirect(to, http.StatusSeeOther)
}
// newMacaron initializes Macaron instance.
func newMacaron() (*macaron.Macaron, error) {
m := macaron.New()
if !conf.Server.DisableRouterLog {
m.Use(macaron.Logger())
}
m.Use(macaron.Recovery())
if conf.Server.EnableGzip {
m.Use(gzip.Gziper())
}
if conf.Server.Protocol == "fcgi" {
m.SetURLPrefix(conf.Server.Subpath)
}
// Register custom middleware first to make it possible to override files under "public".
m.Use(macaron.Static(
filepath.Join(conf.CustomDir(), "public"),
macaron.StaticOptions{
SkipLogging: conf.Server.DisableRouterLog,
},
))
var publicFs http.FileSystem
if !conf.Server.LoadAssetsFromDisk {
publicFs = http.FS(public.Files)
}
m.Use(macaron.Static(
filepath.Join(conf.WorkDir(), "public"),
macaron.StaticOptions{
ETag: true,
SkipLogging: conf.Server.DisableRouterLog,
FileSystem: publicFs,
},
))
m.Use(macaron.Static(
conf.Picture.AvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: conf.UsersAvatarPathPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
m.Use(macaron.Static(
conf.Picture.RepositoryAvatarUploadPath,
macaron.StaticOptions{
ETag: true,
Prefix: database.RepoAvatarURLPrefix,
SkipLogging: conf.Server.DisableRouterLog,
},
))
customDir := filepath.Join(conf.CustomDir(), "templates")
renderOpt := macaron.RenderOptions{
Directory: filepath.Join(conf.WorkDir(), "templates"),
AppendDirectories: []string{customDir},
Funcs: template.FuncMap(),
IndentJSON: macaron.Env != macaron.PROD,
}
if !conf.Server.LoadAssetsFromDisk {
renderOpt.TemplateFileSystem = templates.NewTemplateFileSystem("", customDir)
}
m.Use(macaron.Renderer(renderOpt))
localeNames, err := embedConf.FileNames("locale")
if err != nil {
return nil, errors.Wrap(err, "list locale files")
}
localeFiles := make(map[string][]byte)
for _, name := range localeNames {
localeFiles[name], err = embedConf.Files.ReadFile("locale/" + name)
if err != nil {
return nil, errors.Wrapf(err, "read locale file %q", name)
}
}
m.Use(i18n.I18n(i18n.Options{
SubURL: conf.Server.Subpath,
Files: localeFiles,
CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
Langs: conf.I18n.Langs,
Names: conf.I18n.Names,
DefaultLang: "en-US",
Redirect: true,
}))
m.Use(macaroncache.Cacher(macaroncache.Options{
Adapter: conf.Cache.Adapter,
AdapterConfig: conf.Cache.Host,
Interval: conf.Cache.Interval,
}))
m.Route("/healthcheck", http.MethodHead+","+http.MethodGet, healthCheck)
return m, nil
}
// renderIndex returns the index.html shell with per-request substitutions
// applied for the given WebContext.
func renderIndex(index []byte, wc context.WebContext) ([]byte, error) {
// json.Marshal escapes <, >, and &, so the payload cannot break out of the surrounding <script>.
payload, err := json.Marshal(struct {
Lang string `json:"lang"`
SubURL string `json:"subURL"`
}{
Lang: wc.Lang,
SubURL: wc.SubURL,
})
if err != nil {
return nil, errors.Wrap(err, "marshal web context")
}
script := `<script>window.__webContext=` + string(payload) +
`;document.documentElement.lang=window.__webContext.lang;</script>`
pairs := []string{
"{{.WebContext}}", script,
}
if wc.SubURL != "" {
// Prefix Vite's absolute root paths with the subpath for non-root mounts.
pairs = append(pairs,
`src="/assets/`, `src="`+wc.SubURL+`/assets/`,
`href="/assets/`, `href="`+wc.SubURL+`/assets/`,
`src="/src/`, `src="`+wc.SubURL+`/src/`,
`href="/img/`, `href="`+wc.SubURL+`/img/`,
)
}
return []byte(strings.NewReplacer(pairs...).Replace(string(index))), nil
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
if err := database.Ping(); err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = fmt.Fprintf(w, "* Database connection: %s\n", err)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write([]byte("* Database connection: OK\n"))
}
+182
View File
@@ -0,0 +1,182 @@
package web
import (
"encoding/json"
"net/http"
"net/url"
"path"
"strconv"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"github.com/gogs/git-module"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/gitx"
"gogs.io/gogs/internal/repox"
"gogs.io/gogs/internal/tool"
log "unknwon.dev/clog/v2"
)
// whitespaceFlag maps the `whitespace` query value to its `git diff` flag.
// `ignore-change` (`-b`) still surfaces added/removed blank lines, unlike
// `ignore-all` (`-w`). Empty or unknown values disable whitespace handling.
func whitespaceFlag(v string) string {
switch v {
case "ignore-all":
return "-w"
case "ignore-change":
return "-b"
default:
return ""
}
}
func writeErrorResponse(w http.ResponseWriter, code int, err error) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
message := err.Error()
// Match the JSON-API ReturnHandler: in prod, never leak 5xx detail.
if code >= http.StatusInternalServerError && conf.IsProdMode() {
message = "Internal server error"
}
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
func getRepoCommitRaw(c flamego.Context, repoCtx *repoContext) {
w := c.ResponseWriter()
if !repoCtx.ViewerCanRead() {
writeErrorResponse(w, http.StatusNotFound, errors.New("repository does not exist"))
return
}
owner := repoCtx.Owner
repo := repoCtx.Repo
sha := c.Param("sha")
format := c.Param("format")
gitRepo, err := git.Open(repox.RepositoryPath(owner.Name, repo.Name))
if err != nil {
log.Error("getRepoCommitRaw: open repository %q/%q: %v", owner.Name, repo.Name, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "open repository"))
return
}
if _, err = gitRepo.CatFileCommit(sha); err != nil {
if gitx.IsErrRevisionNotExist(err) {
writeErrorResponse(w, http.StatusNotFound, errors.New("commit does not exist"))
return
}
log.Error("getRepoCommitRaw: cat-file commit %q in %q/%q: %v", sha, owner.Name, repo.Name, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "cat-file commit"))
return
}
var rawOpts []git.RawDiffOptions
if flag := whitespaceFlag(c.Request().URL.Query().Get("whitespace")); flag != "" {
rawOpts = append(rawOpts, git.RawDiffOptions{
CommandOptions: git.CommandOptions{Args: []string{flag}},
})
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
if err = gitRepo.RawDiff(sha, git.RawDiffFormat(format), w, rawOpts...); err != nil {
log.Error("getRepoCommitRaw: get raw diff %s: %v", sha, err)
}
}
// resolveRef resolves ref as a commit SHA, then a branch name, then a tag
// name, in that order. A branch and tag of the same name resolve to the branch.
func resolveRef(gitRepo *git.Repository, ref string) (*git.Commit, error) {
commit, err := gitRepo.CatFileCommit(ref)
if err == nil {
return commit, nil
}
if !gitx.IsErrRevisionNotExist(err) {
return nil, errors.Wrap(err, "cat-file commit")
}
commit, err = gitRepo.BranchCommit(ref)
if err == nil {
return commit, nil
}
if !gitx.IsErrRevisionNotExist(err) {
return nil, errors.Wrap(err, "get branch commit")
}
commit, err = gitRepo.TagCommit(ref)
if err != nil {
return nil, errors.Wrap(err, "get tag commit")
}
return commit, nil
}
func getRepoRawFile(c flamego.Context, repoCtx *repoContext) {
w := c.ResponseWriter()
if !repoCtx.ViewerCanRead() {
writeErrorResponse(w, http.StatusNotFound, errors.New("repository does not exist"))
return
}
owner := repoCtx.Owner
repo := repoCtx.Repo
rawRef := c.Param("ref")
filepath := c.Param("filepath")
ref, err := url.PathUnescape(rawRef)
if err != nil {
writeErrorResponse(w, http.StatusNotFound, errors.New("ref does not exist"))
return
}
gitRepo, err := git.Open(repox.RepositoryPath(owner.Name, repo.Name))
if err != nil {
log.Error("getRepoRawFile: open repository %q/%q: %v", owner.Name, repo.Name, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "open repository"))
return
}
commit, err := resolveRef(gitRepo, ref)
if err != nil {
if gitx.IsErrRevisionNotExist(err) {
writeErrorResponse(w, http.StatusNotFound, errors.New("ref does not exist"))
return
}
log.Error("getRepoRawFile: resolve ref %q in %q/%q: %v", ref, owner.Name, repo.Name, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "resolve ref"))
return
}
blob, err := commit.Blob(filepath)
if err != nil {
if gitx.IsErrRevisionNotExist(err) || errors.Is(err, git.ErrNotBlob) {
writeErrorResponse(w, http.StatusNotFound, errors.New("file does not exist"))
return
}
log.Error("getRepoRawFile: blob %s:%s in %q/%q: %v", commit.ID, filepath, owner.Name, repo.Name, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "get blob"))
return
}
data, err := blob.Bytes()
if err != nil {
log.Error("getRepoRawFile: read blob %s:%s: %v", commit.ID, filepath, err)
writeErrorResponse(w, http.StatusInternalServerError, errors.Wrap(err, "read blob"))
return
}
if pathCommit, err := commit.CommitByPath(git.CommitByRevisionOptions{Path: filepath}); err == nil && pathCommit != nil {
w.Header().Set("Last-Modified", pathCommit.Committer.When.Format(http.TimeFormat))
}
render, _ := strconv.ParseBool(c.Request().URL.Query().Get("render"))
switch {
case !tool.IsTextFile(data) && !tool.IsImageFile(data):
w.Header().Set("Content-Disposition", `attachment; filename="`+path.Base(filepath)+`"`)
w.Header().Set("Content-Transfer-Encoding", "binary")
case tool.IsTextFile(data) && (!conf.Repository.EnableRawFileRenderMode || !render):
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
_, _ = w.Write(data)
}
+219
View File
@@ -0,0 +1,219 @@
package web
import (
stdctx "context"
"encoding/json"
"net/http"
"reflect"
"regexp"
"strings"
"github.com/cockroachdb/errors"
"github.com/flamego/binding"
"github.com/flamego/flamego"
"github.com/flamego/session"
"github.com/flamego/validator"
"github.com/go-macaron/i18n"
macaronsession "github.com/go-macaron/session"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database"
)
type (
webAPIUserKey struct{}
webAPISessionKey struct{}
webAPIMacaronKey struct{}
webAPILocaleKey struct{}
)
func flamegoBridger(webHandler http.Handler) func(c *context.Context, l i18n.Locale) {
return func(c *context.Context, l i18n.Locale) {
ctx := c.Req.Context()
ctx = stdctx.WithValue(ctx, webAPIUserKey{}, c.User)
ctx = stdctx.WithValue(ctx, webAPISessionKey{}, c.Session)
ctx = stdctx.WithValue(ctx, webAPIMacaronKey{}, c.Context)
ctx = stdctx.WithValue(ctx, webAPILocaleKey{}, l)
webHandler.ServeHTTP(c.Resp, c.Req.WithContext(ctx))
}
}
func flamegoInjector(c flamego.Context) {
ctx := c.Request().Context()
user, _ := ctx.Value(webAPIUserKey{}).(*database.User)
sess, _ := ctx.Value(webAPISessionKey{}).(macaronsession.Store)
mc, _ := ctx.Value(webAPIMacaronKey{}).(*macaron.Context)
l, _ := ctx.Value(webAPILocaleKey{}).(i18n.Locale)
c.Map(user, sess, mc, l)
c.MapTo(flamegoSessionAdapter{sess: sess}, (*session.Session)(nil))
}
// flamegoSessionAdapter exposes the underlying Macaron session via the Flamego
// session interface so the captcha middleware (and any future Flamego-native
// session consumer) can read/write the same session store the rest of the app
// uses.
type flamegoSessionAdapter struct {
sess macaronsession.Store
}
func (s flamegoSessionAdapter) ID() string { return s.sess.ID() }
func (s flamegoSessionAdapter) Get(key interface{}) interface{} { return s.sess.Get(key) }
func (s flamegoSessionAdapter) Set(key, val interface{}) { _ = s.sess.Set(key, val) }
func (s flamegoSessionAdapter) SetFlash(val interface{}) { _ = s.sess.Set("_flash", val) }
func (s flamegoSessionAdapter) Delete(key interface{}) { _ = s.sess.Delete(key) }
func (s flamegoSessionAdapter) Flush() { _ = s.sess.Flush() }
func (s flamegoSessionAdapter) Encode() ([]byte, error) { return nil, nil }
func enforceWebAPIMaxBodySize(c flamego.Context) {
r := c.Request().Request
r.Body = http.MaxBytesReader(c.ResponseWriter(), r.Body, 4*1024) // 4 KiB
}
// webAPIValidator is the shared validator instance used by every webapi
// binding. Registering the json-tag name function makes validation errors
// carry the wire field name (e.g. "recoveryCode") via ve.Field(), so the
// 400 payload keys match what the React client sends and reads.
var webAPIValidator = func() *validator.Validate {
v := validator.New()
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
_ = v.RegisterValidation("alphadashdot", func(fl validator.FieldLevel) bool {
return !alphaDashDotInvalid.MatchString(fl.Field().String())
})
return v
}()
var alphaDashDotInvalid = regexp.MustCompile(`[^\d\w\-_\.]`)
// bindJSON binds the request body to T. On binding or validation failure it
// short-circuits with a 400 carrying the standard renderBindingErrors payload,
// so downstream handlers can drop the `if len(bindErrs) > 0` boilerplate and
// the binding.Errors parameter entirely.
func bindJSON(model any) flamego.Handler {
return binding.JSON(model, binding.Options{
Validator: webAPIValidator,
ErrorHandler: func(c flamego.Context, l i18n.Locale, errs binding.Errors) {
w := c.ResponseWriter()
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(renderBindingErrors(l, errs))
},
})
}
func mountWebAPIRoutes(f *flamego.Flame) {
f.Group("/api/web", func() {
f.Group("/user", func() {
f.Get("/info", getUserInfo)
f.Combo("/sign-up").
Get(getUserSignUp).
Post(bindJSON(userSignUpRequest{}), postUserSignUp)
f.Group("/reset-password", func() {
f.Combo("").
Get(getUserResetPassword).
Post(bindJSON(userResetPasswordEmailRequest{}), postUserResetPassword)
f.Post("/complete", bindJSON(userResetPasswordCompleteRequest{}), postUserResetPasswordComplete)
})
f.Combo("/sign-in").
Get(getUserSignIn).
Post(bindJSON(userSignInRequest{}), postUserSignIn)
f.Group("/mfa", func() {
f.Combo("").
Get(getUserMFA).
Post(bindJSON(userMFARequest{}), postUserMFA)
f.Post("/recovery", bindJSON(userMFARecoveryRequest{}), postUserMFARecovery)
})
f.Group("/activate", func() {
f.Combo("").
Get(getUserActivate).
Post(postUserActivate)
f.Post("/complete", bindJSON(userActivateCompleteRequest{}), postUserActivateComplete)
})
f.Post("/sign-out", postUserSignOut)
})
f.Group("/{owner}/{repo}", func() {
f.Get("/header", getRepoHeader)
f.Get("/commit/{sha: /[0-9a-f]{7,40}/}", getRepoCommit)
f.Combo("/watch").Post(postRepoWatch).Delete(deleteRepoWatch)
f.Combo("/star").Post(postRepoStar).Delete(deleteRepoStar)
}, withRepoContext)
}, enforceWebAPIMaxBodySize)
}
// fieldErrors maps JSON field names to per-field localized messages. A non-nil
// value renders inline under the input. A nil value marks the input as
// invalid (highlight + focus eligibility) without duplicating text. Used in
// concert with bindingErrorResponse.Error to surface one banner message while
// highlighting multiple inputs.
type fieldErrors map[string]*string
// bindingErrorResponse carries form-validation failures. Error is the top-level
// message shown as a banner above the form (used when the failure is not tied
// to a specific input, e.g. malformed body, bad credentials).
type bindingErrorResponse struct {
Error string `json:"error,omitempty"`
Fields fieldErrors `json:"fields,omitempty"`
}
// ruleSuffixKeys maps a validator tag to the shared "form.*_error" suffix key
// (e.g. "max" -> "form.max_size_error"). Messages are composed as
// <field label> + <suffix>, mirroring the legacy Macaron binding behavior.
var ruleSuffixKeys = map[string]string{
"required": "form.require_error",
"max": "form.max_size_error",
"min": "form.min_size_error",
"len": "form.size_error",
"email": "form.email_error",
"url": "form.url_error",
"alphadashdot": "form.alpha_dash_dot_error",
}
// renderBindingErrors maps binding.Errors to the response shape, looking up
// localized messages via the request's locale. The per-field label comes from
// "form.<StructField>" (e.g. "form.UserName"); the rule suffix comes from
// ruleSuffixKeys. Rule parameters (e.g. "254" for `max=254`) are passed
// through to the suffix translation for %s expansion. Always HTTP 400.
func renderBindingErrors(l i18n.Locale, errs binding.Errors) *bindingErrorResponse {
for _, e := range errs {
if e.Category == binding.ErrorCategoryDeserialization {
return &bindingErrorResponse{Error: l.Tr("form.invalid_request") + ": " + e.Err.Error()}
}
}
out := make(fieldErrors)
for _, e := range errs {
var ves validator.ValidationErrors
ok := errors.As(e.Err, &ves)
if !ok {
continue
}
for _, ve := range ves {
field := ve.Field()
if _, exists := out[field]; exists {
// Keep the first rule that failed for a given field so the client renders one
// message per input. Subsequent rules surface only after the first is fixed.
continue
}
label := l.Tr("form." + ve.StructField())
suffixKey, known := ruleSuffixKeys[ve.Tag()]
var msg string
switch {
case !known:
msg = l.Tr("form.unknown_error") + " " + ve.Tag()
case ve.Param() != "":
msg = label + l.Tr(suffixKey, ve.Param())
default:
msg = label + l.Tr(suffixKey)
}
out[field] = &msg
}
}
return &bindingErrorResponse{Fields: out}
}
+248
View File
@@ -0,0 +1,248 @@
package web
import (
"context"
"net/http"
"time"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
"github.com/gogs/git-module"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/gitx"
"gogs.io/gogs/internal/repox"
"gogs.io/gogs/internal/strx"
"gogs.io/gogs/internal/tool"
)
type repoHeader struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
AvatarURL string `json:"avatarURL"`
Visibility string `json:"visibility"`
MirrorOf string `json:"mirrorOf,omitempty"`
WatchCount int `json:"watchCount"`
StarCount int `json:"starCount"`
ForkCount int `json:"forkCount"`
IssuesEnabled bool `json:"issuesEnabled"`
OpenIssueCount int `json:"openIssueCount"`
PullRequestsEnabled bool `json:"pullRequestsEnabled"`
OpenPullRequestCount int `json:"openPullRequestCount"`
WikiEnabled bool `json:"wikiEnabled"`
ViewerCanAdminister bool `json:"viewerCanAdminister"`
ViewerIsWatching bool `json:"viewerIsWatching"`
ViewerIsStarring bool `json:"viewerIsStarring"`
}
func getRepoHeader(repoCtx *repoContext) (statusCode int, resp *repoHeader, err error) {
owner := repoCtx.Owner
repo := repoCtx.Repo
issuesEnabled := repo.EnableIssues
wikiEnabled := repo.EnableWiki
if !repoCtx.ViewerCanRead() {
if !repo.IsPartialPublic() {
return http.StatusNotFound, nil, errors.New("repository does not exist")
}
issuesEnabled = repo.CanGuestViewIssues()
wikiEnabled = repo.CanGuestViewWiki()
}
visibility := "public"
if repo.IsPrivate {
visibility = "private"
}
resp = &repoHeader{
ID: repo.ID,
Owner: owner.Name,
Name: repo.Name,
AvatarURL: strx.Coalesce(repo.AvatarLink(), owner.AvatarURL()),
Visibility: visibility,
WatchCount: repo.NumWatches,
StarCount: repo.NumStars,
ForkCount: repo.NumForks,
IssuesEnabled: issuesEnabled,
OpenIssueCount: repo.NumIssues - repo.NumClosedIssues,
PullRequestsEnabled: repo.AllowsPulls(),
OpenPullRequestCount: repo.NumPulls - repo.NumClosedPulls,
WikiEnabled: wikiEnabled,
ViewerCanAdminister: repoCtx.ViewerCanAdminister(),
ViewerIsWatching: database.IsWatching(repoCtx.ViewerID, repo.ID),
ViewerIsStarring: database.IsStarring(repoCtx.ViewerID, repo.ID),
}
if repo.IsMirror {
mirror, err := database.GetMirrorByRepoID(repo.ID)
if err != nil {
log.Error("getRepoHeader: get mirror by repo ID %d: %v", repo.ID, err)
} else if mirror != nil {
resp.MirrorOf = mirror.Address()
}
}
return http.StatusOK, resp, nil
}
type repoCommitSignature struct {
Name string `json:"name"`
Email string `json:"email"`
When time.Time `json:"when"`
AvatarURL string `json:"avatarURL"`
ProfileURL string `json:"profileURL,omitempty"`
}
type repoCommit struct {
SHA string `json:"sha"`
Subject string `json:"subject"`
Body string `json:"body"`
Author repoCommitSignature `json:"author"`
Parents []string `json:"parents"`
}
func getRepoCommit(c flamego.Context, repoCtx *repoContext) (statusCode int, resp *repoCommit, err error) {
if !repoCtx.ViewerCanRead() {
return http.StatusNotFound, nil, errors.New("repository does not exist")
}
ctx := c.Request().Context()
owner := repoCtx.Owner
repo := repoCtx.Repo
commitID := c.Param("sha")
gitRepo, err := git.Open(repox.RepositoryPath(owner.Name, repo.Name))
if err != nil {
log.Error("getRepoCommit: open repository %q/%q: %v", owner.Name, repo.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "open repository")
}
commit, err := gitRepo.CatFileCommit(commitID)
if err != nil {
if gitx.IsErrRevisionNotExist(err) {
return http.StatusNotFound, nil, nil
}
log.Error("getRepoCommit: cat-file commit %q in %q/%q: %v", commitID, owner.Name, repo.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "cat-file commit")
}
parents := make([]string, commit.ParentsCount())
for i := 0; i < commit.ParentsCount(); i++ {
sha, err := commit.ParentID(i)
if err != nil {
log.Error("getRepoCommit: parent ID %d for %q in %q/%q: %v", i, commitID, owner.Name, repo.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "parent ID")
}
parents[i] = sha.String()
}
toSignature := func(s *git.Signature) repoCommitSignature {
sig := repoCommitSignature{
Name: s.Name,
Email: s.Email,
When: s.When.UTC(),
AvatarURL: tool.AvatarLink(s.Email),
}
if u, err := database.Handle.Users().GetByEmail(ctx, s.Email); err == nil && u != nil {
sig.ProfileURL = conf.Server.Subpath + "/" + u.Name
}
return sig
}
subject := commit.Summary()
var body string
if msg := commit.Message; len(msg) > len(subject) {
body = msg[len(subject):]
}
return http.StatusOK, &repoCommit{
SHA: commitID,
Subject: subject,
Body: body,
Author: toSignature(commit.Author),
Parents: parents,
}, nil
}
type repoWatchResponse struct {
WatchCount int `json:"watchCount"`
}
func repoWatchAction(ctx context.Context, repoCtx *repoContext, watching bool) (statusCode int, resp *repoWatchResponse, err error) {
if repoCtx.ViewerCanRead() {
return http.StatusNotFound, nil, errors.New("repository does not exist")
}
repo := repoCtx.Repo
if watching {
err = database.Handle.Repositories().Watch(ctx, repoCtx.ViewerID, repo.ID)
} else {
err = database.WatchRepo(repoCtx.ViewerID, repo.ID, false)
}
if err != nil {
log.Error("repoWatchAction: set watching=%t for user %d on repo %d: %v", watching, repoCtx.ViewerID, repo.ID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "watch repo")
}
updated, err := database.Handle.Repositories().GetByName(ctx, repo.OwnerID, repo.Name)
if err != nil {
log.Error("repoWatchAction: reload repo %d (%q): %v", repo.ID, repo.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "reload repo")
}
return http.StatusOK, &repoWatchResponse{
WatchCount: updated.NumWatches,
}, nil
}
func postRepoWatch(c flamego.Context, repoCtx *repoContext) (statusCode int, resp *repoWatchResponse, err error) {
return repoWatchAction(c.Request().Context(), repoCtx, true)
}
func deleteRepoWatch(c flamego.Context, repoCtx *repoContext) (statusCode int, resp *repoWatchResponse, err error) {
return repoWatchAction(c.Request().Context(), repoCtx, false)
}
type repoStarResponse struct {
StarCount int `json:"starCount"`
}
func repoStarAction(ctx context.Context, repoCtx *repoContext, starring bool) (statusCode int, resp *repoStarResponse, err error) {
if !repoCtx.ViewerCanRead() {
return http.StatusNotFound, nil, errors.New("repository does not exist")
}
repo := repoCtx.Repo
if starring {
err = database.Handle.Repositories().Star(ctx, repoCtx.ViewerID, repo.ID)
} else {
err = database.StarRepo(repoCtx.ViewerID, repo.ID, false)
}
if err != nil {
log.Error("repoStarAction: set starred=%t for user %d on repo %d: %v", starring, repoCtx.ViewerID, repo.ID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "star repo")
}
updated, err := database.Handle.Repositories().GetByName(ctx, repo.OwnerID, repo.Name)
if err != nil {
log.Error("repoStarAction: reload repo %d (%q): %v", repo.ID, repo.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "reload repo")
}
return http.StatusOK, &repoStarResponse{
StarCount: updated.NumStars,
}, nil
}
func postRepoStar(c flamego.Context, repoCtx *repoContext) (statusCode int, resp *repoStarResponse, err error) {
return repoStarAction(c.Request().Context(), repoCtx, true)
}
func deleteRepoStar(c flamego.Context, repoCtx *repoContext) (statusCode int, resp *repoStarResponse, err error) {
return repoStarAction(c.Request().Context(), repoCtx, false)
}
+514
View File
@@ -0,0 +1,514 @@
package web
import (
stdctx "context"
"encoding/hex"
"net/http"
"os"
"strconv"
"time"
"github.com/cockroachdb/errors"
"github.com/flamego/cache"
"github.com/flamego/captcha"
"github.com/flamego/session"
"github.com/go-macaron/i18n"
macaronsession "github.com/go-macaron/session"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/email"
"gogs.io/gogs/internal/tool"
"gogs.io/gogs/internal/userx"
)
func parseUserFromCode(ctx stdctx.Context, code string) (user *database.User) {
if len(code) <= tool.TimeLimitCodeLength {
return nil
}
hexStr := code[tool.TimeLimitCodeLength:]
if b, err := hex.DecodeString(hexStr); err == nil {
if user, err = database.Handle.Users().GetByUsername(ctx, string(b)); user != nil {
return user
} else if !database.IsErrUserNotExist(err) {
log.Error("parseUserFromCode: get user by name %q: %v", string(b), err)
}
}
return nil
}
func verifyUserActiveCode(ctx stdctx.Context, code string) (user *database.User) {
if user = parseUserFromCode(ctx, code); user != nil {
prefix := code[:tool.TimeLimitCodeLength]
data := strconv.FormatInt(user.ID, 10) + user.Email + user.LowerName + user.Password + user.Rands
if tool.VerifyTimeLimitCode(data, conf.Auth.ActivateCodeLives, prefix) {
return user
}
}
return nil
}
type loginSource struct {
ID int64 `json:"id"`
Name string `json:"name"`
IsDefault bool `json:"isDefault"`
}
type getUserSignInResponse struct {
LoginSources []loginSource `json:"loginSources"`
}
type getUserSignUpResponse struct {
RegistrationDisabled bool `json:"registrationDisabled"`
CaptchaEnabled bool `json:"captchaEnabled"`
}
func getUserSignUp() (statusCode int, resp *getUserSignUpResponse, err error) {
return http.StatusOK, &getUserSignUpResponse{
RegistrationDisabled: conf.Auth.DisableRegistration,
CaptchaEnabled: conf.Auth.EnableRegistrationCaptcha,
}, nil
}
type userSignUpRequest struct {
UserName string `json:"userName" validate:"required,alphadashdot,max=35"`
Email string `json:"email" validate:"required,email,max=254"`
Password string `json:"password" validate:"required,max=255"`
Captcha string `json:"captcha"`
}
type userSignUpResponse struct {
EmailConfirmationRequired bool `json:"emailConfirmationRequired,omitempty"`
Email string `json:"email,omitempty"`
Hours int `json:"hours,omitempty"`
}
func postUserSignUp(r *http.Request, mc *macaron.Context, ca cache.Cache, l i18n.Locale, cpt captcha.Captcha, req userSignUpRequest) (statusCode int, resp any, err error) {
if conf.Auth.DisableRegistration {
return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_prompt")}, nil
}
if conf.Auth.EnableRegistrationCaptcha && !cpt.ValidText(req.Captcha) {
msg := l.Tr("form.captcha_incorrect")
return http.StatusUnauthorized, &bindingErrorResponse{
Fields: fieldErrors{"captcha": &msg},
}, nil
}
u, err := database.Handle.Users().Create(
r.Context(),
req.UserName,
req.Email,
database.CreateUserOptions{
Password: req.Password,
Activated: !conf.Auth.RequireEmailConfirmation,
},
)
if err != nil {
switch {
case database.IsErrUserAlreadyExist(err):
msg := l.Tr("form.username_been_taken")
return http.StatusUnprocessableEntity, &bindingErrorResponse{Fields: fieldErrors{"userName": &msg}}, nil
case database.IsErrEmailAlreadyUsed(err):
msg := l.Tr("form.email_been_used")
return http.StatusUnprocessableEntity, &bindingErrorResponse{Fields: fieldErrors{"email": &msg}}, nil
case database.IsErrNameNotAllowed(err):
msg := l.Tr("user.form.name_not_allowed", err.(database.ErrNameNotAllowed).Value())
return http.StatusBadRequest, &bindingErrorResponse{Fields: fieldErrors{"userName": &msg}}, nil
default:
log.Error("postUserSignUp: create user %q: %v", req.UserName, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "create user")
}
}
log.Trace("Account created: %s", u.Name)
if database.Handle.Users().Count(r.Context()) == 1 {
v := true
err := database.Handle.Users().Update(
r.Context(),
u.ID,
database.UpdateUserOptions{
IsActivated: &v,
IsAdmin: &v,
},
)
if err != nil {
log.Error("postUserSignUp: update first user %q: %v", u.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "update user")
}
}
if conf.Auth.RequireEmailConfirmation && u.ID > 1 {
if err := email.SendActivateAccountMail(mc, database.NewMailerUser(u)); err != nil {
log.Error("postUserSignUp: send activation mail to user %q: %v", u.Name, err)
}
if err := ca.Set(r.Context(), userx.MailResendCacheKey(u.ID), 1, 180*time.Second); err != nil {
log.Error("postUserSignUp: put mail resend cache for user %q: %v", u.Name, err)
}
return http.StatusOK, &userSignUpResponse{
EmailConfirmationRequired: true,
Email: u.Email,
Hours: conf.Auth.ActivateCodeLives / 60,
}, nil
}
return http.StatusOK, &userSignUpResponse{}, nil
}
func getUserSignIn(r *http.Request) (statusCode int, resp *getUserSignInResponse, err error) {
sources, err := database.Handle.LoginSources().List(r.Context(), database.ListLoginSourceOptions{OnlyActivated: true})
if err != nil {
log.Error("getUserSignIn: list activated login sources: %v", err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "list activated login sources")
}
loginSources := make([]loginSource, 0, len(sources))
for _, s := range sources {
loginSources = append(loginSources, loginSource{ID: s.ID, Name: s.Name, IsDefault: s.IsDefault})
}
return http.StatusOK, &getUserSignInResponse{LoginSources: loginSources}, nil
}
type userSignInRequest struct {
Username string `json:"username" validate:"required,max=254"`
Password string `json:"password" validate:"required,max=255"`
LoginSource int64 `json:"loginSource"`
}
type getUserResetPasswordResponse struct {
EmailEnabled bool `json:"emailEnabled"`
Valid bool `json:"valid"`
}
func getUserResetPassword(r *http.Request) (statusCode int, resp *getUserResetPasswordResponse, err error) {
code := r.URL.Query().Get("code")
return http.StatusOK, &getUserResetPasswordResponse{
EmailEnabled: conf.Email.Enabled,
Valid: code != "" && verifyUserActiveCode(r.Context(), code) != nil,
}, nil
}
type userResetPasswordEmailRequest struct {
Email string `json:"email" validate:"required,email,max=254"`
}
type userResetPasswordCompleteRequest struct {
Code string `json:"code" validate:"required"`
Password string `json:"password" validate:"required,min=6,max=255"`
}
type userResetPasswordResponse struct {
Hours int `json:"hours,omitempty"`
ResendLimited bool `json:"resendLimited,omitempty"`
}
func postUserResetPassword(r *http.Request, ca cache.Cache, l i18n.Locale, req userResetPasswordEmailRequest) (statusCode int, resp any, err error) {
if !conf.Email.Enabled {
return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_mail")}, nil
}
ctx := r.Context()
u, err := database.Handle.Users().GetByEmail(ctx, req.Email)
if err != nil {
if database.IsErrUserNotExist(err) {
return http.StatusOK, &userResetPasswordResponse{Hours: conf.Auth.ActivateCodeLives / 60}, nil
}
log.Error("postUserResetPassword: get user by email %q: %v", req.Email, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "get user by email")
}
if !u.IsLocal() {
msg := l.Tr("auth.non_local_account")
return http.StatusForbidden, &bindingErrorResponse{Fields: fieldErrors{"email": &msg}}, nil
}
if _, err := ca.Get(ctx, userx.MailResendCacheKey(u.ID)); err == nil {
return http.StatusOK, &userResetPasswordResponse{
Hours: conf.Auth.ActivateCodeLives / 60,
ResendLimited: true,
}, nil
} else if !errors.Is(err, os.ErrNotExist) {
log.Error("postUserResetPassword: get mail resend cache for user %q: %v", u.Name, err)
}
if err = email.SendResetPasswordMail(l, database.NewMailerUser(u)); err != nil {
log.Error("postUserResetPassword: send reset password mail to user %q: %v", u.Name, err)
}
if err = ca.Set(ctx, userx.MailResendCacheKey(u.ID), 1, 180*time.Second); err != nil {
log.Error("postUserResetPassword: put mail resend cache for user %q: %v", u.Name, err)
}
return http.StatusOK, &userResetPasswordResponse{Hours: conf.Auth.ActivateCodeLives / 60}, nil
}
func postUserResetPasswordComplete(r *http.Request, l i18n.Locale, req userResetPasswordCompleteRequest) (statusCode int, resp any, err error) {
u := verifyUserActiveCode(r.Context(), req.Code)
if u == nil {
return http.StatusBadRequest, &bindingErrorResponse{Error: l.Tr("auth.invalid_code")}, nil
}
if err := database.Handle.Users().Update(r.Context(), u.ID, database.UpdateUserOptions{Password: &req.Password}); err != nil {
log.Error("postUserResetPasswordComplete: update password for user %q: %v", u.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "update user")
}
log.Trace("User password reset: %s", u.Name)
return http.StatusNoContent, nil, nil
}
type userSignInResponse struct {
// MFA is true when the account has MFA enabled and the password step
// succeeded but a second factor is still required. The client should
// navigate to /user/mfa to complete the challenge.
MFA bool `json:"mfa,omitempty"`
}
func postUserSignIn(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userSignInRequest) (statusCode int, resp any, err error) {
u, err := database.Handle.Users().Authenticate(r.Context(), req.Username, req.Password, req.LoginSource)
if err != nil {
switch {
case auth.IsErrBadCredentials(err):
return http.StatusUnauthorized, &bindingErrorResponse{
Error: l.Tr("form.username_password_incorrect"),
Fields: fieldErrors{"username": nil, "password": nil},
}, nil
case database.IsErrLoginSourceMismatch(err):
return http.StatusUnprocessableEntity, nil, errors.New(l.Tr("form.auth_source_mismatch"))
default:
log.Error("postUserSignIn: authenticate user %q: %v", req.Username, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "authenticate user")
}
}
if database.Handle.TwoFactors().IsEnabled(r.Context(), u.ID) {
sess.Set("mfaUserID", u.ID)
return http.StatusOK, &userSignInResponse{MFA: true}, nil
}
completeSignIn(sess, mc, u)
return http.StatusOK, &userSignInResponse{}, nil
}
// completeSignIn finalizes the sign-in session for u: writes the auth session,
// clears any in-flight MFA state, and sets the login-status cookie. The
// caller is responsible for navigating to a post-login destination via
// /redirect?to=.
func completeSignIn(sess session.Session, mc *macaron.Context, u *database.User) {
sess.Set("uid", u.ID)
sess.Set("uname", u.Name)
sess.Delete("mfaUserID")
mc.SetCookie(conf.Session.CSRFCookieName, "", -1, conf.Server.Subpath)
if conf.Security.EnableLoginStatusCookie {
mc.SetCookie(conf.Security.LoginStatusCookieName, "true", 0, conf.Server.Subpath)
}
}
func getUserMFA(sess session.Session) (statusCode int, resp any, err error) {
if _, ok := sess.Get("mfaUserID").(int64); !ok {
return http.StatusNotFound, nil, nil
}
return http.StatusNoContent, nil, nil
}
type userMFARequest struct {
Passcode string `json:"passcode" validate:"required,len=6"`
}
type userMFAResponse struct{}
func postUserMFA(r *http.Request, sess session.Session, mc *macaron.Context, ca cache.Cache, l i18n.Locale, req userMFARequest) (statusCode int, resp any, err error) {
userID, ok := sess.Get("mfaUserID").(int64)
if !ok {
return http.StatusUnauthorized, &bindingErrorResponse{Error: l.Tr("auth.mfa_session_expired")}, nil
}
t, err := database.Handle.TwoFactors().GetByUserID(r.Context(), userID)
if err != nil {
log.Error("postUserMFA: get two factor by user ID %d: %v", userID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "get two factor by user ID")
}
valid, err := t.ValidateTOTP(req.Passcode)
if err != nil {
log.Error("postUserMFA: validate TOTP for user %d: %v", userID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "validate TOTP")
}
if !valid {
msg := l.Tr("auth.mfa_invalid_passcode")
return http.StatusUnauthorized, &bindingErrorResponse{
Fields: fieldErrors{"passcode": &msg},
}, nil
}
cacheKey := userx.TwoFactorCacheKey(userID, req.Passcode)
if _, err := ca.Get(r.Context(), cacheKey); err == nil {
msg := l.Tr("auth.mfa_reused_passcode")
return http.StatusUnauthorized, &bindingErrorResponse{
Fields: fieldErrors{"passcode": &msg},
}, nil
} else if !errors.Is(err, os.ErrNotExist) {
log.Error("postUserMFA: get two factor passcode cache for user %d: %v", userID, err)
}
if err = ca.Set(r.Context(), cacheKey, 1, 60*time.Second); err != nil {
log.Error("postUserMFA: cache two factor passcode for user %d: %v", userID, err)
}
u, err := database.Handle.Users().GetByID(r.Context(), userID)
if err != nil {
log.Error("postUserMFA: get user by ID %d: %v", userID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "get user by ID")
}
completeSignIn(sess, mc, u)
return http.StatusOK, &userMFAResponse{}, nil
}
type userMFARecoveryRequest struct {
RecoveryCode string `json:"recoveryCode" validate:"required,len=11"`
}
func postUserMFARecovery(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userMFARecoveryRequest) (statusCode int, resp any, err error) {
userID, ok := sess.Get("mfaUserID").(int64)
if !ok {
return http.StatusUnauthorized, &bindingErrorResponse{Error: l.Tr("auth.mfa_session_expired")}, nil
}
if err := database.Handle.TwoFactors().UseRecoveryCode(r.Context(), userID, req.RecoveryCode); err != nil {
if database.IsTwoFactorRecoveryCodeNotFound(err) {
msg := l.Tr("auth.mfa_invalid_recovery_code")
return http.StatusUnauthorized, &bindingErrorResponse{
Fields: fieldErrors{"recoveryCode": &msg},
}, nil
}
log.Error("postUserMFARecovery: use recovery code for user %d: %v", userID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "use recovery code")
}
u, err := database.Handle.Users().GetByID(r.Context(), userID)
if err != nil {
log.Error("postUserMFARecovery: get user by ID %d: %v", userID, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "get user by ID")
}
completeSignIn(sess, mc, u)
return http.StatusOK, &userMFAResponse{}, nil
}
type userInfo struct {
Username string `json:"username"`
AvatarURL string `json:"avatarURL"`
IsAdmin bool `json:"isAdmin"`
CanCreateOrganization bool `json:"canCreateOrganization"`
}
func getUserInfo(user *database.User) (statusCode int, resp *userInfo, err error) {
if user == nil {
return http.StatusNoContent, nil, nil
}
return http.StatusOK,
&userInfo{
Username: user.Name,
AvatarURL: user.AvatarURL(),
IsAdmin: user.IsAdmin,
CanCreateOrganization: user.CanCreateOrganization(),
},
nil
}
type getUserActivateResponse struct {
Email string `json:"email,omitempty"`
CodeLifetimeHours int `json:"codeLifetimeHours,omitempty"`
}
func getUserActivate(u *database.User) (statusCode int, resp any, err error) {
if u == nil {
return http.StatusUnauthorized, nil, nil
}
// An already-active and authenticated user has no business on the activation page.
if u.IsActive {
return http.StatusNotFound, nil, nil
}
return http.StatusOK, &getUserActivateResponse{
Email: u.Email,
CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60,
}, nil
}
type postUserActivateResponse struct {
RateLimited bool `json:"rateLimited,omitempty"`
CodeLifetimeHours int `json:"codeLifetimeHours,omitempty"`
}
func postUserActivate(r *http.Request, u *database.User, mc *macaron.Context, ca cache.Cache, l i18n.Locale) (statusCode int, resp any, err error) {
if u == nil {
return http.StatusUnauthorized, nil, nil
}
if u.IsActive {
return http.StatusNotFound, nil, nil
}
if !conf.Auth.RequireEmailConfirmation {
return http.StatusForbidden, &bindingErrorResponse{Error: l.Tr("auth.disable_register_mail")}, nil
}
ctx := r.Context()
if _, err := ca.Get(ctx, userx.MailResendCacheKey(u.ID)); err == nil {
return http.StatusOK, &postUserActivateResponse{
RateLimited: true,
CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60,
}, nil
} else if !errors.Is(err, os.ErrNotExist) {
log.Error("postUserActivate: get mail resend cache for user %q: %v", u.Name, err)
}
if err := email.SendActivateAccountMail(mc, database.NewMailerUser(u)); err != nil {
log.Error("postUserActivate: send activation mail to user %q: %v", u.Name, err)
}
if err := ca.Set(ctx, userx.MailResendCacheKey(u.ID), 1, 180*time.Second); err != nil {
log.Error("postUserActivate: put mail resend cache for user %q: %v", u.Name, err)
}
return http.StatusOK, &postUserActivateResponse{CodeLifetimeHours: conf.Auth.ActivateCodeLives / 60}, nil
}
type userActivateCompleteRequest struct {
Code string `json:"code" validate:"required"`
}
func postUserActivateComplete(r *http.Request, sess session.Session, mc *macaron.Context, l i18n.Locale, req userActivateCompleteRequest) (statusCode int, resp any, err error) {
target := verifyUserActiveCode(r.Context(), req.Code)
if target == nil {
return http.StatusBadRequest, &bindingErrorResponse{Error: l.Tr("auth.invalid_code")}, nil
}
v := true
if err := database.Handle.Users().Update(
r.Context(),
target.ID,
database.UpdateUserOptions{
GenerateNewRands: true,
IsActivated: &v,
},
); err != nil {
log.Error("postUserActivateComplete: update user %q: %v", target.Name, err)
return http.StatusInternalServerError, nil, errors.Wrap(err, "update user")
}
log.Trace("User activated: %s", target.Name)
completeSignIn(sess, mc, target)
return http.StatusNoContent, nil, nil
}
type postUserSignOutResponse struct {
RedirectTo string `json:"redirectTo,omitempty"`
}
func postUserSignOut(sess macaronsession.Store, mc *macaron.Context) (statusCode int, resp *postUserSignOutResponse, err error) {
_ = sess.Flush()
_ = sess.Destory(mc)
mc.SetCookie(conf.Session.CSRFCookieName, "", -1, conf.Server.Subpath)
if conf.Auth.CustomLogoutURL != "" {
return http.StatusOK, &postUserSignOutResponse{RedirectTo: conf.Auth.CustomLogoutURL}, nil
}
return http.StatusNoContent, nil, nil
}
+62
View File
@@ -0,0 +1,62 @@
//go:build !prod
package web
import (
"bytes"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/context"
)
func mountWebAppRoutes(f *flamego.Flame) error {
viteURL, err := url.Parse("http://localhost:5173")
if err != nil {
return errors.Wrap(err, "parse Vite URL")
}
proxy := httputil.NewSingleHostReverseProxy(viteURL)
proxy.ModifyResponse = func(resp *http.Response) error {
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
return nil
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "read Vite response body")
}
_ = resp.Body.Close()
wc := context.WebContextFrom(resp.Request)
body, err := renderIndex(raw, wc)
if err != nil {
log.Error("Failed to render index: %v", err)
body = []byte("Internal Server Error\n")
resp.StatusCode = http.StatusInternalServerError
resp.Status = http.StatusText(http.StatusInternalServerError)
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
} else if wc.StatusCode > 0 {
resp.StatusCode = wc.StatusCode
resp.Status = http.StatusText(wc.StatusCode)
}
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Header.Set("Content-Length", strconv.Itoa(len(body)))
// The upstream validators describe the unmodified body. Drop them
// so the browser does not satisfy a conditional request from a
// cached copy that has a stale injected lang attribute.
resp.Header.Del("ETag")
resp.Header.Del("Last-Modified")
return nil
}
f.Any("/{**}", func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
return nil
}
+68
View File
@@ -0,0 +1,68 @@
//go:build prod
package web
import (
"io/fs"
"net/http"
"github.com/cockroachdb/errors"
"github.com/flamego/flamego"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/public"
)
func mountWebAppRoutes(f *flamego.Flame) error {
webFS, err := fs.Sub(public.WebAssets, "dist")
if err != nil {
return errors.Wrap(err, "load embedded web assets")
}
// Prefix matches the path rewrites renderIndex applies to the index
// shell. Without it the browser fetches /<subpath>/assets/... and the
// static handler looks them up in webFS at "<subpath>/assets/...",
// which has no <subpath> directory, so every asset would 404 and fall
// through to the wildcard handler as text/html.
//
// Index is set to a sentinel that does not exist in the FS so flamego.Static
// never serves the raw index.html for "/" requests. The catch-all below
// always renders the shell through renderIndex instead, ensuring template
// substitutions are applied.
f.Use(flamego.Static(flamego.StaticOptions{
FileSystem: http.FS(webFS),
Prefix: conf.Server.Subpath,
Index: "__disabled__",
}))
index, err := public.WebAssets.ReadFile("dist/index.html")
if err != nil {
return errors.Wrap(err, `read "dist/index.html"`)
}
f.Get("/{**}", func(w http.ResponseWriter, r *http.Request) {
wc := context.WebContextFrom(r)
body, err := renderIndex(index, wc)
if err != nil {
log.Error("Failed to render index: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// The body is rewritten per request (lang injection, future
// runtime config), so caching it would serve stale content to
// any user whose request resolves to a different locale. Use
// no-store rather than no-cache so the browser cannot keep a
// copy at all, not even for revalidation. Static assets keep
// their normal caching via flamego.Static.
w.Header().Set("Cache-Control", "no-store")
status := wc.StatusCode
if status <= 0 {
status = http.StatusOK
}
w.WriteHeader(status)
_, _ = w.Write(body)
})
return nil
}
+55
View File
@@ -0,0 +1,55 @@
// Gogs is a painless self-hosted Git Service.
package main
import (
"context"
"os"
"path/filepath"
"github.com/urfave/cli/v3"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/cmd/gogs/internal/web"
"gogs.io/gogs/internal/conf"
)
func init() {
conf.App.Version = "0.15.0+dev"
}
var webCommand = cli.Command{
Name: "web",
Usage: "Start the web server",
Description: "Serves the web interface, API, and HTTP Git endpoints.",
Action: func(_ context.Context, cmd *cli.Command) error {
var portOverride int
if cmd.IsSet("port") {
portOverride = cmd.Int("port")
}
return web.Run(configFromLineage(cmd), portOverride)
},
Flags: []cli.Flag{
intFlag("port, p", 3000, "Alternative listening port to use"),
stringFlag("config, c", filepath.Join(conf.CustomDir(), "conf", "app.ini"), "Custom configuration file path"),
},
}
func main() {
cmd := &cli.Command{
Name: "Gogs",
Usage: "The painless way to host your own Git service",
Version: conf.App.Version,
Commands: []*cli.Command{
&webCommand,
&servCommand,
&hookCommand,
&adminCommand,
&importCommand,
&backupCommand,
&restoreCommand,
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal("Failed to start application: %v", err)
}
}
+22 -22
View File
@@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@@ -8,17 +8,17 @@ import (
"github.com/cockroachdb/errors"
"github.com/unknwon/cae/zip"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
"gopkg.in/ini.v1"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/database"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/semverutil"
"gogs.io/gogs/internal/osx"
"gogs.io/gogs/internal/semverx"
)
var Restore = cli.Command{
var restoreCommand = cli.Command{
Name: "restore",
Usage: "Restore files and database from backup",
Description: `Restore imports all related files and database from a backup archive.
@@ -42,11 +42,11 @@ be skipped and remain unchanged.`,
// format that is able to import.
var lastSupportedVersionOfFormat = map[int]string{}
func runRestore(c *cli.Context) error {
zip.Verbose = c.Bool("verbose")
func runRestore(ctx context.Context, cmd *cli.Command) error {
zip.Verbose = cmd.Bool("verbose")
tmpDir := c.String("tempdir")
if !osutil.IsDir(tmpDir) {
tmpDir := cmd.String("tempdir")
if !osx.IsDir(tmpDir) {
log.Fatal("'--tempdir' does not exist: %s", tmpDir)
}
archivePath := path.Join(tmpDir, archiveRootDir)
@@ -58,15 +58,15 @@ func runRestore(c *cli.Context) error {
}
defer func() { _ = os.RemoveAll(archivePath) }()
log.Info("Restoring backup from: %s", c.String("from"))
err = zip.ExtractTo(c.String("from"), tmpDir)
log.Info("Restoring backup from: %s", cmd.String("from"))
err = zip.ExtractTo(cmd.String("from"), tmpDir)
if err != nil {
log.Fatal("Failed to extract backup archive: %v", err)
}
// Check backup version
metaFile := filepath.Join(archivePath, "metadata.ini")
if !osutil.IsFile(metaFile) {
if !osx.IsFile(metaFile) {
log.Fatal("File 'metadata.ini' is missing")
}
metadata, err := ini.Load(metaFile)
@@ -74,7 +74,7 @@ func runRestore(c *cli.Context) error {
log.Fatal("Failed to load metadata '%s': %v", metaFile, err)
}
backupVersion := metadata.Section("").Key("GOGS_VERSION").MustString("999.0")
if semverutil.Compare(conf.App.Version, "<", backupVersion) {
if semverx.Compare(conf.App.Version, "<", backupVersion) {
log.Fatal("Current Gogs version is lower than backup version: %s < %s", conf.App.Version, backupVersion)
}
formatVersion := metadata.Section("").Key("VERSION").MustInt()
@@ -90,9 +90,9 @@ func runRestore(c *cli.Context) error {
// Otherwise, it's optional to set config file flag.
configFile := filepath.Join(archivePath, "custom", "conf", "app.ini")
var customConf string
if c.IsSet("config") {
customConf = c.String("config")
} else if !osutil.IsFile(configFile) {
if lineageConf := configFromLineage(cmd); lineageConf != "" {
customConf = lineageConf
} else if !osx.IsFile(configFile) {
log.Fatal("'--config' is not specified and custom config file is not found in backup")
} else {
customConf = configFile
@@ -111,13 +111,13 @@ func runRestore(c *cli.Context) error {
// Database
dbDir := path.Join(archivePath, "db")
if err = database.ImportDatabase(context.Background(), conn, dbDir, c.Bool("verbose")); err != nil {
if err = database.ImportDatabase(ctx, conn, dbDir, cmd.Bool("verbose")); err != nil {
log.Fatal("Failed to import database: %v", err)
}
if !c.Bool("database-only") {
if !cmd.Bool("database-only") {
// Custom files
if osutil.IsDir(conf.CustomDir()) {
if osx.IsDir(conf.CustomDir()) {
if err = os.Rename(conf.CustomDir(), conf.CustomDir()+".bak"); err != nil {
log.Fatal("Failed to backup current 'custom': %v", err)
}
@@ -131,12 +131,12 @@ func runRestore(c *cli.Context) error {
for _, dir := range []string{"attachments", "avatars", "repo-avatars"} {
// Skip if backup archive does not have corresponding data
srcPath := filepath.Join(archivePath, "data", dir)
if !osutil.IsDir(srcPath) {
if !osx.IsDir(srcPath) {
continue
}
dirPath := filepath.Join(conf.Server.AppDataPath, dir)
if osutil.IsDir(dirPath) {
if osx.IsDir(dirPath) {
if err = os.Rename(dirPath, dirPath+".bak"); err != nil {
log.Fatal("Failed to backup current 'data': %v", err)
}
@@ -149,7 +149,7 @@ func runRestore(c *cli.Context) error {
// Repositories
reposPath := filepath.Join(archivePath, "repositories.zip")
if !c.Bool("exclude-repos") && !c.Bool("database-only") && osutil.IsFile(reposPath) {
if !cmd.Bool("exclude-repos") && !cmd.Bool("database-only") && osx.IsFile(reposPath) {
if err := zip.ExtractTo(reposPath, filepath.Dir(conf.Repository.Root)); err != nil {
log.Fatal("Failed to extract 'repositories.zip': %v", err)
}
+12 -17
View File
@@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@@ -6,11 +6,11 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/unknwon/com"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
@@ -21,7 +21,7 @@ const (
accessDeniedMessage = "Repository does not exist or you do not have access"
)
var Serv = cli.Command{
var servCommand = cli.Command{
Name: "serv",
Usage: "This command should only be called by SSH shell",
Description: `Serv provide access auth for repositories`,
@@ -48,15 +48,10 @@ func fail(userMessage, errMessage string, args ...any) {
os.Exit(1)
}
func setup(c *cli.Context, logFile string, connectDB bool) {
func setup(cmd *cli.Command, logFile string, connectDB bool) {
conf.HookMode = true
var customConf string
if c.IsSet("config") {
customConf = c.String("config")
} else if c.GlobalIsSet("config") {
customConf = c.GlobalString("config")
}
customConf := configFromLineage(cmd)
err := conf.Init(customConf)
if err != nil {
@@ -128,16 +123,15 @@ var allowedCommands = map[string]database.AccessMode{
"git-receive-pack": database.AccessModeWrite,
}
func runServ(c *cli.Context) error {
ctx := context.Background()
setup(c, "serv.log", true)
func runServ(ctx context.Context, cmd *cli.Command) error {
setup(cmd, "serv.log", true)
if conf.SSH.Disabled {
println("Gogs: SSH has been disabled")
return nil
}
if len(c.Args()) < 1 {
if cmd.Args().Len() < 1 {
fail("Not enough arguments", "Not enough arguments")
}
@@ -188,9 +182,10 @@ func runServ(c *cli.Context) error {
// Allow anonymous (user is nil) clone for public repositories.
var user *database.User
key, err := database.GetPublicKeyByID(com.StrTo(strings.TrimPrefix(c.Args()[0], "key-")).MustInt64())
keyID, _ := strconv.ParseInt(strings.TrimPrefix(cmd.Args().Get(0), "key-"), 10, 64)
key, err := database.GetPublicKeyByID(keyID)
if err != nil {
fail("Invalid key ID", "Invalid key ID '%s': %v", c.Args()[0], err)
fail("Invalid key ID", "Invalid key ID '%s': %v", cmd.Args().Get(0), err)
}
if requestMode == database.AccessModeWrite || repo.IsPrivate {
-16
View File
@@ -1,16 +0,0 @@
coverage:
range: "60...95"
status:
project:
default:
threshold: 1%
informational: true
patch:
default:
only_pulls: true
informational: true
comment:
layout: 'diff'
github_checks: false
+13 -15
View File
@@ -141,8 +141,7 @@ FILE_MAX_SIZE = 3
MAX_FILES = 5
[database]
; The database backend, either "postgres", "mysql" "sqlite3" or "mssql".
; You can connect to TiDB with MySQL protocol.
; The database backend, either "postgres", "mysql" or "sqlite3".
TYPE = postgres
HOST = 127.0.0.1:5432
NAME = gogs
@@ -165,12 +164,8 @@ INSTALL_LOCK = false
; The secret to encrypt cookie values, 2FA code, etc.
; !!CHANGE THIS TO KEEP YOUR USER DATA SAFE!!
SECRET_KEY = !#@FDEWREWR&*(
; The days remembered for auto-login.
; The number of days a sign-in session persists across browser restarts.
LOGIN_REMEMBER_DAYS = 7
; The cookie name to store auto-login information.
COOKIE_REMEMBER_NAME = gogs_incredible
; The cookie name to store logged in username.
COOKIE_USERNAME = gogs_awesome
; Whether to set secure cookie.
COOKIE_SECURE = false
; Whether to set cookie to indicate user login status.
@@ -197,8 +192,6 @@ USER = noreply@gogs.localhost
; The login password.
PASSWORD =
; Whether to disable HELO operation when the hostname is different.
DISABLE_HELO =
; The custom hostname for HELO operation, default is from system.
HELO_HOSTNAME =
@@ -236,6 +229,9 @@ ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
; The HTTP header used as username for reverse proxy authentication.
REVERSE_PROXY_AUTHENTICATION_HEADER = X-WEBAUTH-USER
; Lists the IPs or CIDR ranges whose requests are allowed to set the reverse
; proxy authentication header.
TRUSTED_PROXY_IPS = 127.0.0.0/8,::1/128
[user]
; Whether to enable email notifications for users.
@@ -255,19 +251,20 @@ COOKIE_NAME = i_like_gogs
COOKIE_SECURE = false
; The GC interval in seconds for session data.
GC_INTERVAL = 3600
; The maximum life time in seconds for a session.
MAX_LIFE_TIME = 86400
; The maximum idle time in seconds before a session record is garbage-collected.
; Set lower than `[security] LOGIN_REMEMBER_DAYS * 86400` to enforce a sliding
; idle timeout. Otherwise the session lives for the full cookie lifetime.
MAX_LIFE_TIME = 604800
; The cookie name for CSRF token.
CSRF_COOKIE_NAME = _csrf
[cache]
; The cache adapter, either "memory", "redis", or "memcache".
; The cache adapter, either "memory" or "redis".
ADAPTER = memory
; For "memory" only, GC interval in seconds.
INTERVAL = 60
; For "redis" and "memcache", connection host address:
; For "redis", connection host address:
; - redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180
; - memcache: `127.0.0.1:11211`
HOST =
[http]
@@ -279,6 +276,8 @@ ACCESS_CONTROL_ALLOW_ORIGIN =
STORAGE = local
; The root path to store LFS objects on local file system.
OBJECTS_PATH = data/lfs-objects
; The path to temporarily store LFS objects during upload verification.
OBJECTS_TEMP_PATH = data/tmp/lfs-objects
[attachment]
; Whether to enabled upload attachments in general.
@@ -570,6 +569,5 @@ mn-MN = mn
ro-RO = ro
[other]
SHOW_FOOTER_BRANDING = false
; Show time of template execution in the footer
SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
+151 -48
View File
@@ -1,13 +1,13 @@
app_desc = A painless self-hosted Git service
app_desc = The painless way to host your own Git service
home = Home
dashboard = Dashboard
explore = Explore
help = Help
sign_in = Sign In
sign_out = Sign Out
sign_up = Sign Up
register = Register
sign_in = Sign in
sign_out = Sign out
sign_up = Sign up
register = Create account
website = Website
page = Page
template = Template
@@ -17,35 +17,53 @@ user_profile_and_more = User profile and more
signed_in_as = Signed in as
username = Username
username_placeholder = Enter your username or email
new_username_placeholder = Choose a username
email = Email
email_placeholder = Enter your email
password = Password
re_type = Re-Type
password_placeholder = Enter your password
captcha = Captcha
captcha_placeholder = Enter the characters shown above
captcha_image_alt = Captcha image
refresh_captcha = Refresh captcha
click_to_refresh_captcha = Click to refresh
repository = Repository
organization = Organization
mirror = Mirror
new_repo = New Repository
new_migrate = New Migration
new_mirror = New Mirror
new_fork = New Fork Repository
new_org = New Organization
manage_org = Manage Organizations
admin_panel = Admin Panel
new_repo = New repository
new_migrate = New migration
new_mirror = New mirror
new_fork = New fork repository
new_org = New organization
manage_org = Manage organizations
admin_panel = Admin panel
account_settings = Account Settings
settings = Settings
your_profile = Your Profile
your_settings = Your Settings
theme = Theme
theme_light = Light
theme_dark = Dark
theme_system = System
your_profile = Your profile
your_settings = Your settings
activities = Activities
pull_requests = Pull Requests
pull_requests = Pull requests
issues = Issues
cancel = Cancel
close = Close
show_more = Show more
show_less = Show less
resize_sidebar = Resize sidebar
more = More
more_tabs = More tabs
more_actions = More actions
[status]
page_not_found = Page Not Found
internal_server_error = Internal Server Error
page_not_found = Page not found
internal_server_error = Internal server error
[install]
install = Installation
@@ -116,11 +134,10 @@ admin_setting_desc = You don't need to create an admin account right now. The fi
admin_title = Admin Account Settings
admin_name = Username
admin_password = Password
confirm_password = Confirm Password
confirm_password = Confirm password
admin_email = Admin Email
install_gogs = Install Gogs
test_git_failed = Failed to test 'git' command: %v
sqlite3_not_available = Your release version does not support SQLite3, please download the official binary version from %s, NOT the gobuild version.
invalid_db_setting = Database setting is not correct: %v
invalid_repo_path = Repository root path is invalid: %v
run_user_not_match = Run user isn't the current user: %s -> %s
@@ -152,39 +169,68 @@ organizations = Organizations
search = Search
[auth]
create_new_account = Create New Account
create_new_account = Create new account
sign_up_submitting = Creating account...
sign_up_failed = Could not create account, please try again.
sign_in_submitting = Signing in...
sign_in_failed = Could not sign in, please try again.
show_password = Show password
hide_password = Hide password
back_to_sign_in = Back to sign in
mfa_title = Multi-factor authentication
mfa_passcode = Passcode
mfa_passcode_placeholder = Enter the 6-digit code from your authenticator
mfa_recovery_code = Recovery code
mfa_recovery_code_placeholder = Enter a recovery code
mfa_use_recovery_code = Use a recovery code instead
mfa_use_passcode = Use a passcode instead
mfa_verify = Verify
mfa_verifying = Verifying...
mfa_session_expired = Your sign-in session has expired. Please sign in again.
mfa_verify_failed = Verification failed. Please try again.
mfa_invalid_passcode = The passcode you entered is not valid.
mfa_reused_passcode = The passcode you entered has already been used, please try another one.
mfa_invalid_recovery_code = Recovery code already used or invalid.
register_hepler_msg = Already have an account? Sign in now!
social_register_hepler_msg = Already have an account? Bind now!
disable_register_prompt = Sorry, registration has been disabled. Please contact the site administrator.
disable_register_mail = Sorry, email services are disabled. Please contact the site administrator.
auth_source = Authentication Source
auth_source = Authentication source
local = Local
remember_me = Remember Me
forgot_password= Forgot Password
forget_password = Forgot password?
sign_up_now = Need an account? Sign up now.
confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the registration process.
active_your_account = Activate Your Account
sign_up_now = Create a new account
activate_your_account = Activate your account
prohibit_login = Login Prohibited
prohibit_login_desc = Your account is prohibited from logging in. Please contact the site admin.
resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
resend_rate_limited = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to receive a new one, please click the button below.
resend_mail = Click here to resend your activation email
send_reset_mail = Click here to (re)send your password reset email
reset_password = Reset Your Password
invalid_code = Sorry, your confirmation code has expired or not valid.
reset_password_helper = Click here to reset your password
password_too_short = Password length must be at least 6 characters.
send_activation_email = Send activation email
check_activation_email = Please check your email and click the activation link to finish creating your account.
activation_email_pending = Your email address <email>{email}</email> is not yet confirmed. Click below to send a new activation email valid for <hours>{hours} hours</hours>.
activation_email_sent = A new activation email has been sent to <email>{email}</email>. Please check your inbox within <hours>{hours} hours</hours>.
sending_activation_email = Sending activation email...
send_activation_email_failed = Could not send activation email, please try again.
activating_account = Activating your account...
send_reset_email = Send password reset email
reset_password_email_submitting = Sending password reset email...
reset_password_email_failed = Could not send password reset email, please try again.
reset_password_email_sent = A password reset email has been sent to <email>{email}</email>, please check your inbox within <hours>{hours} hours</hours>.
reset_password = Reset your password
invalid_code = The confirmation code has expired or not valid.
reset_password_submit = Reset password
reset_password_submitting = Resetting password...
reset_password_resend_limited = You already requested a password reset email recently. Please wait 3 minutes then try again.
reset_password_failed = Could not reset password, please try again.
new_password = New password
new_password_placeholder = Enter your new password
confirm_password = Confirm password
confirm_password_placeholder = Re-enter your password
confirm_new_password = Confirm new password
confirm_new_password_placeholder = Re-enter your new password
password_mismatch = The two passwords do not match.
non_local_account = Non-local accounts cannot change passwords through Gogs.
login_two_factor = Two-factor Authentication
login_two_factor_passcode = Authentication Passcode
login_two_factor_enter_recovery_code = Enter a two-factor recovery code
login_two_factor_recovery = Two-factor Recovery
login_two_factor_recovery_code = Recovery Code
login_two_factor_enter_passcode = Enter a two-factor passcode
login_two_factor_invalid_recovery_code = Recovery code already used or invalid.
[mail]
activate_account = Please activate your account
activate_email = Verify your email address
@@ -198,7 +244,9 @@ no = No
modify = Modify
[form]
invalid_request = The request could not be processed
UserName = Username
Username = Username
RepoName = Repository name
Email = Email address
Password = Password
@@ -209,6 +257,8 @@ PayloadUrl = Payload URL
TeamName = Team name
AuthName = Authorization name
AdminEmail = Admin email
Passcode = Passcode
RecoveryCode = Recovery code
NewBranchName = New branch name
CommitSummary = Commit summary
@@ -236,7 +286,7 @@ repo_name_been_taken = Repository name has already been taken.
org_name_been_taken = Organization name has already been taken.
team_name_been_taken = Team name has already been taken.
email_been_used = Email address has already been used.
username_password_incorrect = Username or password is not correct.
username_password_incorrect = Username or password is incorrect.
auth_source_mismatch = The authentication source selected is not associated with the user.
enterred_invalid_repo_name = Please make sure that the repository name you entered is correct.
enterred_invalid_owner_name = Please make sure that the owner name you entered is correct.
@@ -459,7 +509,32 @@ unwatch = Unwatch
watch = Watch
unstar = Unstar
star = Star
starred = Starred
fork = Fork
mirror_of = mirror of
sign_in_to_watch = Sign in to watch this repository
sign_in_to_star = Sign in to star this repository
sign_in_to_fork = Sign in to fork this repository
watch_this_repository = Watch this repository
unwatch_this_repository = Unwatch this repository
star_this_repository = Star this repository
unstar_this_repository = Unstar this repository
fork_this_repository = Fork this repository
visibility_private = This repository is private
visibility_public = This repository is public
view_watchers = View watchers
view_stargazers = View stargazers
view_forks = View forks
browse_files = Browse files
view_history = View history
view_raw = View raw
copy_file_path = Copy file path
copy_full_sha = Copy full SHA
renamed_from = Renamed from
authored = authored
parents = parents
diff_label = diff
patch_label = patch
no_desc = No Description
quick_guide = Quick Guide
@@ -888,9 +963,39 @@ diff.show_split_view = Split View
diff.show_unified_view = Unified View
diff.stats_desc = <strong> %d changed files</strong> with <strong>%d additions</strong> and <strong>%d deletions</strong>
diff.bin = BIN
diff.view_file = View File
diff.view_file = View file
diff.file_suppressed = File diff suppressed because it is too large
diff.too_many_files = Some files were not shown because too many files changed in this diff
diff.showing_changed_files = Showing <count>{count} changed files</count>
diff.additions = additions
diff.deletions = deletions
diff.unified = Unified
diff.split = Split
diff.diff_settings = Diff settings
diff.whitespace = Whitespace
diff.show_whitespace = Show whitespace
diff.ignore_whitespace_changes = Ignore whitespace changes
diff.ignore_all_whitespace = Ignore all whitespace
diff.display = Display
diff.wrap_long_lines = Wrap long line
diff.expand_all_files = Expand all files
diff.collapse_all_files = Collapse all files
show_file_tree = Show file tree
hide_file_tree = Hide file tree
expand_all_directories = Expand all directories
collapse_all_directories = Collapse all directories
search_files = Search files
search_hide = Hide search
search_diff = Search in diff
search_previous_match = Previous match
search_next_match = Next match
commit_parent = parent
commit_label = commit
view_file = View file
diff.expand_file = Expand file
diff.collapse_file = Collapse file
diff.expand_all_lines = Expand all lines
diff.all_lines_expanded = All lines expanded
release.releases = Releases
release.new_release = New Release
@@ -1086,7 +1191,7 @@ users.created = Created
users.send_register_notify = Send Registration Notification To User
users.new_success = New account '%s' has been created successfully.
users.edit = Edit
users.auth_source = Authentication Source
users.auth_source = Authentication source
users.local = Local
users.auth_login_name = Authentication Login Name
users.password_helper = Leave it empty to remain unchanged.
@@ -1119,7 +1224,7 @@ repos.stars = Stars
repos.issues = Issues
repos.size = Size
auths.auth_sources = Authentication Sources
auths.auth_sources = Authentication sources
auths.new = Add New Source
auths.name = Name
auths.type = Type
@@ -1249,8 +1354,6 @@ config.db.max_idle_conns = Maximum idle connections
config.security_config = Security configuration
config.security.login_remember_days = Login remember days
config.security.cookie_remember_name = Remember cookie
config.security.cookie_username = Username cookie
config.security.cookie_secure = Enable secure cookie
config.security.reverse_proxy_auth_user = Reverse proxy authentication header
config.security.enable_login_status_cookie = Enable login status cookie
@@ -1263,7 +1366,6 @@ config.email.subject_prefix = Subject prefix
config.email.host = Host
config.email.from = From
config.email.user = User
config.email.disable_helo = Disable HELO
config.email.helo_hostname = HELO hostname
config.email.skip_verify = Skip certificate verify
config.email.use_certificate = Use custom certificate
@@ -1286,6 +1388,7 @@ config.auth.enable_registration_captcha = Enable registration captcha
config.auth.enable_reverse_proxy_authentication = Enable reverse proxy authentication
config.auth.enable_reverse_proxy_auto_registration = Enable reverse proxy auto registration
config.auth.reverse_proxy_authentication_header = Reverse proxy authentication header
config.auth.trusted_proxy_ips = Trusted proxy IPs
config.user_config = User configuration
config.user.enable_email_notify = Enable email notification
-20
View File
@@ -1,20 +0,0 @@
#!/bin/sh
set -xe
if [ "$(uname -m)" = "aarch64" ]; then
export arch='arm64'
export checksum='17f325293d08f6f964e0530842e9ef1410dd5f83ee6475b493087391032b0cfd'
elif [ "$(uname -m)" = "armv7l" ]; then
export arch='arm'
export checksum='e5b0261e9f6563ce3ace9e038520eb59d2c77c8d85f2b47ab41e1fe7cf321528'
else
export arch='amd64'
export checksum='a35462ec71410cccfc428072de830e4478bc57a919d0131ef7897759270dff8f'
fi
wget --quiet https://github.com/go-task/task/releases/download/v3.40.1/task_linux_${arch}.tar.gz -O task_linux_${arch}.tar.gz
echo "${checksum} task_linux_${arch}.tar.gz" | sha256sum -cs
tar -xzf task_linux_${arch}.tar.gz
mv task /usr/local/bin/task
+26
View File
@@ -0,0 +1,26 @@
## Development
Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command:
```
pnpm i -g mint
```
Run the following command at the root of your documentation, where your `docs.json` is located:
```
mint dev
```
View your local preview at `http://localhost:3000`.
## Need help?
### Troubleshooting
- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI.
- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`.
### Resources
- [Mintlify documentation](https://mintlify.com/docs)
-21
View File
@@ -1,21 +0,0 @@
# Configuring Git Large File Storage (LFS)
> NOTE: Git LFS is supported in Gogs starting with version 0.12.
Git LFS works out of box with default configuration for any supported versions.
## Known limitations
- Only local storage is supported (i.e. all LFS objects are stored on the same server where Gogs runs), support of Object Storage Service like Amazon S3 is being tracked in [#6065](https://github.com/gogs/gogs/issues/6065).
## Configuration
All configuration options for Git LFS are located in [`[lfs]` section](https://github.com/gogs/gogs/blob/44ea9604ed7440c2cf1105d965c2429ee225e8f6/conf/app.ini#L266-L270):
```ini
[lfs]
; The storage backend for uploading new objects.
STORAGE = local
; The root path to store LFS objects on local file system.
OBJECTS_PATH = data/lfs-objects
```
-33
View File
@@ -1,33 +0,0 @@
# Release strategy
## Semantic versioning
Starting 0.12.0, Gogs uses [semantic versioning](https://semver.org/) for publishing releases. For example:
- `0.12.0` is a minor version release.
- `0.12.1` is the first patch release of `0.12`.
- `0.12` indicates a series of releases for a minor version and its patch releases.
Each minor release has its own release branch with prefix `release/`, e.g. `release/0.12` is the release branch for minor version 0.12.0 and all its patch releases (`0.12.1`, `0.12.2`, etc.).
## Backwards compatibility
### Before 0.12
If you're running Gogs with any version below 0.12, please upgrade to 0.12 to run necessary migrations.
### Since 0.12
We maintain one minor version backwards compatibility, patch releases are disregarded.
For example, you should:
- Upgrade from `0.12.0` to `0.13.0`.
- Upgrade from `0.12.1` to `0.13.4`.
- NOT upgrade from `0.12.4` to `0.14.0`.
Therefore, we recommend upgrade one minor version at a time.
### Running source builds
If you're running Gogs with building from source code, we recommend you update at least weekly to be not fall behind and potentially miss migrations.
+270
View File
@@ -0,0 +1,270 @@
---
title: "Authentication"
description: "Integrate with your existing IAM system"
icon: "key"
---
Gogs supports authentication through various external sources. Currently supported backends are **LDAP**, **SMTP**, **PAM**, and **HTTP header**. Authentication sources can be configured in two ways:
- **Admin Panel**: Navigate to **Admin Panel > Authentication Sources**
- **Configuration files**: Place `.conf` files in the `custom/conf/auth.d/` directory. Each file describes one source using INI format. Files are loaded once at startup and keyed by `id`. See the "Configuration file" subsection under each backend below for examples.
## LDAP
Gogs supports two variants of LDAP authentication: **Simple Auth** and **Bind DN**. In both cases, authentication is performed by attempting to bind to the LDAP server with the User DN and password. The difference is that with Bind DN, a preliminary query is performed (using the Bind DN credentials) to find the User DN first.
<Tabs>
<Tab title="When to use Bind DN">
The Bind DN mechanism has these advantages:
- It may be more secure than blindly attempting to bind with a possibly non-existent User DN.
- It supports login with attributes such as email address or phone number. The preliminary search can look up the User DN using `mail` or `mobile` attributes.
- It is required when the LDAP does not allow the User DN to query its own attributes or group memberships.
The downside is that, unless the LDAP allows anonymous queries, it requires a bind DN to be defined and Gogs needs to store its credentials. Gogs currently does not encrypt these credentials.
</Tab>
<Tab title="When to use Simple Auth">
In the ideal situation where you know the exact DN template for your users and the LDAP allows the User DN to query its own attributes, Simple Auth is the simpler option. It requires no separate bind account and no stored credentials beyond what the user provides at login.
</Tab>
</Tabs>
### Shared fields
The following fields are shared between both **Bind DN** and **Simple Auth** configurations:
| Field | Required | Description | Example |
|---|---|---|---|
| **Authentication Name** | Yes | A friendly name for the authentication source. | `My LDAP` |
| **Security Protocol** | Yes | Connection security: Unencrypted, LDAPS, or StartTLS. | `LDAPS` |
| **Host** | Yes | The address of the LDAP server. | `ldap.mydomain.com` |
| **Port** | Yes | The port for the LDAP connection. Usually `389` for LDAP/StartTLS, `636` for LDAPS. | `389` |
| **User Filter** | Yes | An LDAP filter declaring which users can log in. The `%s` parameter is substituted with the login name. | `(&(objectClass=posixAccount)(uid=%s))` |
| **Email Attribute** | Yes | The LDAP attribute containing the user's email address. | `mail` |
| **Admin Filter** | No | An LDAP filter applied to the User DN context to determine Gogs administrator privileges. | `(memberOf=cn=admins,cn=groups,dc=mydomain,dc=com)` |
| **Username Attribute** | No | The LDAP attribute containing the username. Used for the Gogs account name after first sign-in. Leave empty to use the login name from the sign-in form. | `uid` |
| **First Name Attribute** | No | The LDAP attribute containing the user's first name. | `givenName` |
| **Surname Attribute** | No | The LDAP attribute containing the user's last name. | `sn` |
<Tip>
The **User Filter** field can be used to filter on group membership if the User DN object has `memberOf` attributes. For example:
```
(&(objectClass=posixAccount)(uid=%s)(memberOf=cn=gogs_users,cn=groups,dc=mydomain,dc=com))
```
In the Bind DN authenticator, the User Filter can also match against multiple user attributes:
```
(&(objectClass=Person)(|(uid=%s)(mail=%s)(mobile=%s)))
```
</Tip>
### Simple Auth fields
LDAP via Simple Auth adds the following field:
| Field | Required | Description | Example |
|---|---|---|---|
| **User DN** | Yes | A template for the user's DN. The `%s` parameter is substituted with the login name. | `cn=%s,ou=Users,dc=mydomain,dc=com` or `uid=%s,ou=Users,dc=mydomain,dc=com` |
### Bind DN fields
LDAP via Bind DN adds the following fields:
| Field | Required | Description | Example |
|---|---|---|---|
| **Bind DN** | No | The DN used to bind to the LDAP server when searching for the user. Leave blank for anonymous search. | `cn=Search,dc=mydomain,dc=com` |
| **Bind Password** | No | The password for the Bind DN specified above. | -- |
| **User Search Base** | Yes | The LDAP base below which user accounts will be searched. | `ou=Users,dc=mydomain,dc=com` |
| **Fetch Attributes in Bind DN Context** | No | When enabled, user attributes are retrieved while bound as the Bind DN instead of the User DN. | -- |
<Warning>
The Bind Password is stored in plaintext on the server. Ensure that your Bind DN has the minimum privileges necessary.
</Warning>
### Group membership verification
You can optionally verify LDAP group membership using the following fields:
| Field | Required | Description | Example |
|---|---|---|---|
| **Group Search Base DN** | No | The LDAP base below which groups will be searched. | `ou=group,dc=mydomain,dc=com` |
| **Group Filter** | No | An LDAP filter declaring the groups that grant access. | `(\|(cn=gogs_users)(cn=admins))` |
| **Group Attribute Containing List of Users** | No | The multi-valued attribute containing the group's members. | `memberUid` or `member` |
| **User Attribute Listed in Group** | No | The user attribute referenced in the group membership attributes. | `uid` or `dn` |
### Configuration files
LDAP sources can also be defined as `.conf` files in `custom/conf/auth.d/` instead of through the admin panel. Files are loaded at startup and keyed by `id`.
<Tabs>
<Tab title="Bind DN">
```ini
id = 101
type = ldap_bind_dn
name = LDAP BindDN
is_activated = true
[config]
host = mydomain.com
port = 636
# 0 - Unencrypted, 1 - LDAPS, 2 - StartTLS
security_protocol = 0
skip_verify = false
bind_dn =
bind_password =
user_base = ou=Users,dc=mydomain,dc=com
attribute_username =
attribute_name =
attribute_surname =
attribute_mail = mail
attributes_in_bind = false
filter = (&(objectClass=posixAccount)(cn=%s))
admin_filter =
group_enabled = false
group_dn =
group_filter =
group_member_uid =
user_uid =
```
</Tab>
<Tab title="Simple Auth">
```ini
id = 102
type = ldap_simple_auth
name = LDAP Simple Auth
is_activated = true
[config]
host = mydomain.com
port = 636
# 0 - Unencrypted, 1 - LDAPS, 2 - StartTLS
security_protocol = 0
skip_verify = false
bind_dn =
bind_password =
user_base =
user_dn = cn=%s,ou=Users,dc=mydomain,dc=com
attribute_username =
attribute_name =
attribute_surname =
attribute_mail = mail
attributes_in_bind = false
filter = (&(objectClass=posixAccount)(cn=%s))
admin_filter =
group_enabled = false
group_dn =
group_filter =
group_member_uid =
user_uid =
```
</Tab>
</Tabs>
### FreeIPA examples
It is possible to use either Bind DN or Simple Auth with FreeIPA. The examples below assume your domain is `domain.com` and that users must be a member of the `gogs_users` group to get access.
<AccordionGroup>
<Accordion title="FreeIPA with Simple Auth">
Setting up access using Simple Auth is straightforward:
```ini
user_dn = uid=%s,cn=users,cn=accounts,dc=domain,dc=com
filter = (&(objectClass=posixAccount)(memberOf=cn=gogs_users,cn=groups,cn=accounts,dc=domain,dc=com))
attribute_username = uid
attribute_name = givenName
attribute_surname = sn
attribute_mail = mail
admin_filter = (memberOf=cn=admins,cn=groups,cn=accounts,dc=domain,dc=com)
group_enabled = false
```
</Accordion>
<Accordion title="FreeIPA with Bind DN">
If you want to allow login by email address, note that FreeIPA by default does not grant anonymous search access to the `mail` attribute. This can be changed in IPA:
```bash
ipa permission-mod --includedattrs=mail 'System: Read User Standard Attributes'
```
Alternatively, you can ask your LDAP administrators for a dedicated bind user account.
<Info>
Allowing email-based login via Bind DN may no longer be necessary. Gogs translates email logins to the corresponding user ID before making the authentication call to the backend LDAP. The only requirement is that the user's **first login** is with their user ID. After that, they can use either user ID or email address.
</Info>
More precisely, Gogs maps the login name onto the user's "Authentication Login Name", which administrators can edit on the user's **Edit Account** page.
</Accordion>
</AccordionGroup>
## PAM
To configure PAM authentication, set the **PAM Service Name** to a filename in `/etc/pam.d/`.
<Warning>
If you want PAM authentication to work with normal Linux passwords, the user running Gogs must have read access to `/etc/shadow`.
</Warning>
### Configuration file
```ini
id = 104
type = pam
name = System Auth
is_activated = true
[config]
service_name = system-auth
```
## SMTP
SMTP authentication allows Gogs to log in to your SMTP host to verify user credentials. Configure the following fields:
| Field | Required | Description | Example |
|---|---|---|---|
| **Authentication Name** | Yes | A name for this authentication source. | `Company SMTP` |
| **SMTP Authentication Type** | Yes | The authentication type: `PLAIN` or `LOGIN`. | `PLAIN` |
| **Host** | Yes | The address of the SMTP server. | `smtp.mydomain.com` |
| **Port** | Yes | The port for the SMTP connection. | `587` |
| **Allowed Domains** | No | Restrict login to specific email domains. Separate multiple domains with commas. | `gogs.io,mydomain.com` |
| **Enable TLS Encryption** | No | Enable TLS encryption for the authentication connection. | -- |
| **Skip TLS Verify** | No | Disable TLS certificate verification. | -- |
| **This Authentication is Activated** | No | Enable or disable this authentication method. | -- |
### Configuration file
```ini
id = 103
type = smtp
name = GMail
is_activated = true
[config]
# Either "PLAIN" or "LOGIN"
auth = PLAIN
host = smtp.gmail.com
port = 587
allowed_domains =
tls = true
skip_verify = false
```
## HTTP header
If your reverse proxy already handles user authentication (e.g. via SSO, OAuth, or client certificates), Gogs can trust the authenticated username from an HTTP header. This is configured in `custom/conf/app.ini` under `[auth]`:
```ini
[auth]
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
REVERSE_PROXY_AUTHENTICATION_HEADER = X-WEBAUTH-USER
```
| Option | Default | Description |
|--------|---------|-------------|
| `ENABLE_REVERSE_PROXY_AUTHENTICATION` | `false` | Enable reading the authenticated username from a request header. |
| `REVERSE_PROXY_AUTHENTICATION_HEADER` | `X-WEBAUTH-USER` | The HTTP header containing the authenticated username. |
| `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION` | `false` | Automatically create a Gogs account for users that do not yet exist. |
When auto-registration is enabled, Gogs creates new accounts with an activated status and a placeholder email address. The user can update their email after first login.
<Warning>
Only enable this feature if Gogs is exclusively accessed through a trusted reverse proxy that sets the header. Exposing Gogs directly to the internet with this enabled would allow anyone to impersonate any user by setting the header themselves.
</Warning>
+67
View File
@@ -0,0 +1,67 @@
---
title: "CLI reference"
description: "Discover all the commands available in the gogs binary"
icon: "terminal"
---
Most people know `gogs web` for starting the server, but the `gogs` binary ships with several other commands that help you manage your instance from the command line.
Run `gogs --help` at any time to see the full list of available commands, and `gogs <command> --help` for details on a specific command.
<Tip>
Every command accepts a `--config` (`-c`) flag to specify a custom configuration file path. The default is `custom/conf/app.ini`.
</Tip>
## Starting the server
```bash
gogs web
```
The `web` command starts the HTTP server that powers the web UI, the REST API, and Git HTTP operations. Use the `--port` (`-p`) flag to override the default listening port.
## Administration
```bash
gogs admin <subcommand>
```
The `admin` command lets you perform maintenance tasks without going through the web interface. Available subcommands include:
| Subcommand | Purpose |
|---|---|
| `create-user` | Create a new user account (with optional `--admin` flag). |
| `delete-inactive-users` | Remove user accounts that were never activated. |
| `delete-repository-archives` | Clean up generated repository archive files. |
| `delete-missing-repositories` | Remove database records for repositories whose Git data is missing on disk. |
| `collect-garbage` | Run `git gc` across all repositories. |
| `rewrite-authorized-keys` | Regenerate the SSH `authorized_keys` file from the database. |
| `resync-hooks` | Re-write Git server-side hooks for all repositories. |
| `reinit-missing-repositories` | Re-initialize bare Git repositories that are missing on disk. |
<Warning>
`rewrite-authorized-keys` replaces the entire `authorized_keys` file. Any non-Gogs keys in that file will be lost.
</Warning>
## Importing data
```bash
gogs import locale --source <dir> --target <dir>
```
The `import` command helps you bring portable data from other Gogs installations into your local instance. Currently the only subcommand is `locale`, which merges locale files from a source directory into a target directory.
## Backup and restore
```bash
gogs backup
gogs restore --from <archive>
```
`backup` dumps the database, repositories, and related files into a single zip archive. `restore` imports everything back from an archive, which is useful for migrating Gogs to another server or switching database engines.
Both commands support `--database-only` and `--exclude-repos` flags to narrow the scope. `backup` additionally supports `--exclude-mirror-repos` and `--target` to control where the archive is saved.
## Internal commands
The `serv` and `hook` commands are used internally by the SSH and Git subsystems. You generally do not need to invoke them directly, but they are the reason Gogs can handle SSH authentication and server-side Git hooks without any external tooling.
+100
View File
@@ -0,0 +1,100 @@
---
title: "Custom templates"
description: "Override HTML templates, static files, and inject custom content"
icon: "paintbrush"
---
Gogs allows you to customize the appearance and behavior of your instance by overriding HTML templates, replacing static files, and injecting custom content. All customizations are placed under the `custom/` directory and survive code updates.
<Warning>
Be careful when overriding templates and static files, as changes to the upstream Gogs codebase may break your customizations in future releases. Keep track of what you have overridden.
</Warning>
## Override HTML templates
You can replace any HTML template (including email templates) by placing a customized version under the `custom/templates/` directory.
<Steps>
<Step title="Find the original template">
Locate the template file you want to customize in the `templates/` directory of the Gogs source code. For example, to customize the home page, find `templates/home.tmpl`.
</Step>
<Step title="Copy and edit">
Copy the content of the template file and save your edited version to the corresponding path under `custom/templates/`. For example:
```
custom/templates/home.tmpl
```
</Step>
<Step title="Restart Gogs">
Edits to custom HTML templates **require restarting Gogs** to take effect.
</Step>
</Steps>
<Warning>
Override for email templates is disabled when `[server] LOAD_ASSETS_FROM_DISK = true` is set in your configuration. If you are using this setting, email template overrides will not be applied.
</Warning>
## Override static files
You can replace static files (CSS, JavaScript, images, etc.) by placing customized versions under the `custom/public/` directory.
For example, to override the site favicon, place your version at:
```
custom/public/img/favicon.png
```
<Tip>
Edits to custom static files **do not** require restarting Gogs. Changes take effect immediately.
</Tip>
## Inject custom content
You can inject custom HTML into the head or footer of every page without touching the main repository source code. This is useful for adding analytics code, custom stylesheets, or other static resources.
This approach is **recommended whenever possible** because it has the minimum impact on templates and is less likely to break during upgrades.
The injection points are:
| File | Location | Purpose |
|---|---|---|
| `custom/templates/inject/head.tmpl` | Inside `<head>` | Add stylesheets, meta tags, analytics scripts |
| `custom/templates/inject/footer.tmpl` | Before `</body>` | Add scripts, tracking code, custom footer content |
### Example: custom CSS file
The following example shows how to include a custom CSS file in your Gogs instance:
<Steps>
<Step title="Create the CSS file">
Create a file named `custom.css` under the `custom/public/css/` directory:
```
custom/public/css/custom.css
```
</Step>
<Step title="Add your CSS rules">
Write your CSS rules in the file. For example:
```css
/* custom/public/css/custom.css */
.dashboard .news .news-item .header {
color: #333;
}
footer {
background-color: #f5f5f5;
}
```
</Step>
<Step title="Link the stylesheet">
Edit the file `custom/templates/inject/head.tmpl` and add a link to your CSS file:
```html
<link rel="stylesheet" href="/css/custom.css">
```
</Step>
<Step title="Restart Gogs">
Restart Gogs to load the new `head.tmpl` injection template. After the initial restart, future edits to the custom CSS file **do not** require restarting Gogs.
</Step>
</Steps>
+102
View File
@@ -0,0 +1,102 @@
---
title: "Git LFS"
description: "Managing large binary files with some magic"
icon: "file-arrow-up"
---
Git Large File Storage (LFS) helps manage large binary files in Git repositories. Instead of storing large files directly in the repository, Git LFS replaces them with lightweight pointers while storing the actual file contents on a separate server.
## How it works
The Git LFS client communicates with the Gogs server over HTTP/HTTPS. It uses HTTP Basic Authentication to authorize client requests. Once a request is authorized, the Git LFS client receives instructions on where to fetch or push the large file.
## Server configuration
Git LFS works out of the box with the default configuration for any supported version of Gogs.
All configuration options for Git LFS are located in the `[lfs]` section of `custom/conf/app.ini`:
```ini
[lfs]
; The storage backend for uploading new objects.
STORAGE = local
; The root path to store LFS objects on the local file system.
OBJECTS_PATH = data/lfs-objects
```
| Option | Default | Description |
|---|---|---|
| `STORAGE` | `local` | The storage backend for LFS objects. Currently only `local` is supported. |
| `OBJECTS_PATH` | `data/lfs-objects` | The root path on the local file system where LFS objects are stored. |
## Version requirements
To use Git LFS with your Gogs instance, you need:
- Gogs version **0.12** or later
- [Git LFS client](https://git-lfs.github.com/) version **1.0.1** or later
## Using Git LFS
Git LFS endpoints in a Gogs server are automatically discovered by the Git LFS client, so you do not need to configure anything upfront.
<Steps>
<Step title="Install Git LFS">
Install the [Git LFS client](https://git-lfs.github.com/) on your machine. Most package managers include it:
```bash
# macOS
brew install git-lfs
# Debian/Ubuntu
sudo apt install git-lfs
# Then initialize Git LFS
git lfs install
```
</Step>
<Step title="Track large files">
In your repository, tell Git LFS which file patterns to track:
```bash
git lfs track "*.psd"
git lfs track "*.zip"
```
This creates or updates a `.gitattributes` file. Make sure to commit it:
```bash
git add .gitattributes
git commit -m "Track large files with Git LFS"
```
</Step>
<Step title="Push as usual">
Add, commit, and push your files normally. Git LFS will automatically handle the large files:
```bash
git add design.psd
git commit -m "Add design file"
git push origin main
```
</Step>
</Steps>
For a complete walkthrough, see the official [Git LFS Tutorial](https://github.com/git-lfs/git-lfs/wiki/Tutorial).
## Known limitations
<Warning>
Be aware of the following limitations when using Git LFS with Gogs.
</Warning>
<AccordionGroup>
<Accordion title="No S3 or object storage support">
Only local storage is supported. All LFS objects are stored on the same server where Gogs runs. Support for Object Storage Services like Amazon S3 is being tracked in [gogs/gogs#6065](https://github.com/gogs/gogs/issues/6065).
</Accordion>
<Accordion title="SSH remotes use HTTP for LFS transfers">
When SSH is set as a remote, Git LFS objects still go through HTTP/HTTPS. Any Git LFS request will prompt for HTTP/HTTPS credentials, so a good Git credentials store is recommended.
</Accordion>
<Accordion title="No file locking support">
File locking is not supported. This feature is being tracked in [gogs/gogs#6064](https://github.com/gogs/gogs/issues/6064).
</Accordion>
</AccordionGroup>
+78
View File
@@ -0,0 +1,78 @@
---
title: "Localization"
description: "Configure interface languages and contribute translations to Gogs"
icon: "language"
---
Gogs has supported multiple languages since release `v0.5.0`. Users can change the interface language instantly with a single click from their settings page.
## Configuration
Available languages are configured in `custom/conf/app.ini` under the `[i18n]` section. All supported languages are enabled by default:
```ini
[i18n]
LANGS = en-US,zh-CN,zh-HK,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT
NAMES = English,简体中文,繁體中文,Deutsch,Français,Nederlands,Latviešu,Русский,日本語,Español,Português do Brasil,Polski,български,Italiano
```
| Option | Description |
|---|---|
| `LANGS` | A comma-separated list of locale codes to enable. Each entry corresponds to a locale file. |
| `NAMES` | A comma-separated list of display names for each language, in the same order as `LANGS`. |
<Tip>
To restrict the available languages, simply remove entries from both `LANGS` and `NAMES`. Make sure the two lists remain in the same order and have the same number of entries.
</Tip>
## Contributing translations
Translations are managed through Crowdin. To contribute:
<Steps>
<Step title="Sign up">
Create an account on the [Gogs Crowdin project](https://crowdin.gogs.io/).
</Step>
<Step title="Translate">
Browse the available strings and fill in untranslated entries for your language.
</Step>
<Step title="Review">
Review existing translations and suggest improvements where needed.
</Step>
</Steps>
<Info>
When translating, focus on conveying the meaning rather than producing a literal word-for-word translation. It is more important that the translation reads naturally in your language than that it matches the exact words of the English version.
</Info>
### Making corrections
If you find an incorrectly translated string, you can search for it efficiently on [Crowdin](https://crowdin.gogs.io/) by using its **key name** rather than the translated text.
For example:
- To fix the translation for "Home", search for the key `home` instead of searching for the word "Home".
- For keys under a section, search using the format `section:key_name`, such as `home:uname_holder`.
### Testing translations locally
If you want to test your translation without making changes to your Git history, place your locale file into:
```
custom/conf/locale/<file>
```
Then restart Gogs to load the updated translations.
## Custom locale files
If you are not satisfied with the official translation for your language, you can override individual fields by creating a custom locale file:
```
custom/conf/locale/locale_<lang>.ini
```
For example, to override specific English strings, create `custom/conf/locale/locale_en-US.ini` and add only the keys you want to change. Restart Gogs to apply the changes.
<Note>
Custom locale files only need to contain the keys you want to override, not the entire locale file. Unspecified keys will fall back to the official translation.
</Note>
+130
View File
@@ -0,0 +1,130 @@
---
title: "Webhooks"
description: "Stay informed for repository events"
icon: "bell"
---
Gogs supports moonlanding for repository events, allowing your external services to receive HTTP notifications when actions occur in your repositories. All event pushes are **POST requests**.
## Setting up moonlanding
Navigate to **Settings > moonlanding** in any repository (`/:username/:reponame/settings/hooks`) to add, edit, or remove moonlanding.
## Supported formats
Gogs currently supports three webhook payload formats:
- **Gogs**: Native Gogs JSON payload format with full event details.
- **Slack**: Slack-compatible payload format for posting to Slack channels.
- **Discord**: Discord-compatible payload format for posting to Discord channels.
## Event headers
Every webhook delivery includes the following HTTP headers:
| Header | Description | Example |
|---|---|---|
| `X-Gogs-Delivery` | A unique UUID identifying this delivery. | `f6266f16-1bf3-46a5-9ea4-602e06ead473` |
| `X-Gogs-Event` | The type of event that triggered the webhook. | `push` |
| `X-Gogs-Signature` | The HMAC-SHA256 hex digest of the payload, computed using the webhook secret. Use this to verify that the payload was sent by Gogs. | `1921679ed627...` |
<Tip>
Always verify the `X-Gogs-Signature` header in your webhook receiver to ensure the request genuinely originated from your Gogs instance.
</Tip>
## Example payload
The following is an example of the event information and JSON payload sent by Gogs for a **push** event:
**Request headers:**
```http
X-Gogs-Delivery: f6266f16-1bf3-46a5-9ea4-602e06ead473
X-Gogs-Event: push
X-Gogs-Signature: 1921679ed6274399b6514721056337f6913b6ff1cb35a24d340e983745d637f1
```
**Request body:**
```json
{
"ref": "refs/heads/main",
"before": "28e1879d029cb852e4844d9c718537df08844e03",
"after": "bffeb74224043ba2feb48d137756c8a9331c449a",
"compare_url": "https://gogs.example.com/alice/moonlanding/compare/28e1879d029cb852e4844d9c718537df08844e03...bffeb74224043ba2feb48d137756c8a9331c449a",
"commits": [
{
"id": "bffeb74224043ba2feb48d137756c8a9331c449a",
"message": "Update README\n",
"url": "https://gogs.example.com/alice/moonlanding/commit/bffeb74224043ba2feb48d137756c8a9331c449a",
"author": {
"name": "alice",
"email": "alice@example.com",
"username": "alice"
},
"committer": {
"name": "alice",
"email": "alice@example.com",
"username": "alice"
},
"timestamp": "2017-03-13T13:52:11-04:00"
}
],
"repository": {
"id": 140,
"owner": {
"id": 1,
"login": "alice",
"full_name": "alice",
"email": "alice@example.com",
"avatar_url": "https://secure.gravatar.com/avatar/d8b2871cdac01b57bbda23716cc03b96",
"username": "alice"
},
"name": "moonlanding",
"full_name": "alice/moonlanding",
"description": "",
"private": false,
"fork": false,
"html_url": "https://gogs.example.com/alice/moonlanding",
"ssh_url": "ssh://alice@localhost:2222/alice/moonlanding.git",
"clone_url": "https://gogs.example.com/alice/moonlanding.git",
"website": "",
"stars_count": 0,
"forks_count": 1,
"watchers_count": 1,
"open_issues_count": 7,
"default_branch": "main",
"created_at": "2017-02-26T04:29:06-05:00",
"updated_at": "2017-03-13T13:51:58-04:00"
},
"pusher": {
"id": 1,
"login": "alice",
"full_name": "alice",
"email": "alice@example.com",
"avatar_url": "https://secure.gravatar.com/avatar/d8b2871cdac01b57bbda23716cc03b96",
"username": "alice"
},
"sender": {
"id": 1,
"login": "alice",
"full_name": "alice",
"email": "alice@example.com",
"avatar_url": "https://secure.gravatar.com/avatar/d8b2871cdac01b57bbda23716cc03b96",
"username": "alice"
}
}
```
### Payload fields
| Field | Description |
|---|---|
| `ref` | The full Git reference that was pushed to (e.g., `refs/heads/main`). |
| `before` | The SHA of the commit at the head of the branch before the push. |
| `after` | The SHA of the commit at the head of the branch after the push. |
| `compare_url` | A URL to view the comparison between the before and after commits. |
| `commits` | An array of commit objects included in the push. |
| `repository` | The full repository object with metadata. |
| `pusher` | The user who performed the push. |
| `sender` | The user who triggered the event. |
@@ -0,0 +1,4 @@
---
title: "Add or update team repository"
openapi: "PUT /admin/teams/{teamid}/repos/{reponame}"
---
@@ -0,0 +1,4 @@
---
title: "Add team membership"
openapi: "PUT /admin/teams/{teamid}/members/{username}"
---
@@ -0,0 +1,4 @@
---
title: "Create a new user"
openapi: "POST /admin/users"
---
@@ -0,0 +1,4 @@
---
title: "Create a public key for a user"
openapi: "POST /admin/users/{username}/keys"
---
@@ -0,0 +1,4 @@
---
title: "Create a repository for a user"
openapi: "POST /admin/users/{username}/repos"
---
@@ -0,0 +1,4 @@
---
title: "Create a team"
openapi: "POST /admin/orgs/{orgname}/teams"
---
@@ -0,0 +1,4 @@
---
title: "Create an organization"
openapi: "POST /admin/users/{username}/orgs"
---
@@ -0,0 +1,4 @@
---
title: "Delete a user"
openapi: "DELETE /admin/users/{username}"
---
@@ -0,0 +1,4 @@
---
title: "Edit an existing user"
openapi: "PATCH /admin/users/{username}"
---
@@ -0,0 +1,4 @@
---
title: "List all members of a team"
openapi: "GET /admin/teams/{teamid}/members"
---
@@ -0,0 +1,4 @@
---
title: "Remove team membership"
openapi: "DELETE /admin/teams/{teamid}/members/{username}"
---
@@ -0,0 +1,4 @@
---
title: "Remove team repository"
openapi: "DELETE /admin/teams/{teamid}/repos/{reponame}"
---
@@ -0,0 +1,4 @@
---
title: "Add a collaborator"
openapi: "PUT /repos/{owner}/{repo}/collaborators/{collaborator}"
---
@@ -0,0 +1,4 @@
---
title: "Add a deploy key"
openapi: "POST /repos/{owner}/{repo}/keys"
---
@@ -0,0 +1,4 @@
---
title: "Check if a user is a collaborator"
openapi: "GET /repos/{owner}/{repo}/collaborators/{collaborator}"
---
@@ -0,0 +1,4 @@
---
title: "Get a deploy key"
openapi: "GET /repos/{owner}/{repo}/keys/{id}"
---
@@ -0,0 +1,4 @@
---
title: "List collaborators"
openapi: "GET /repos/{owner}/{repo}/collaborators"
---
@@ -0,0 +1,4 @@
---
title: "List deploy keys"
openapi: "GET /repos/{owner}/{repo}/keys"
---
@@ -0,0 +1,4 @@
---
title: "Remove a collaborator"
openapi: "DELETE /repos/{owner}/{repo}/collaborators/{collaborator}"
---
@@ -0,0 +1,4 @@
---
title: "Remove a deploy key"
openapi: "DELETE /repos/{owner}/{repo}/keys/{id}"
---
+104
View File
@@ -0,0 +1,104 @@
---
title: "Introduction"
sidebarTitle: "Introduction"
description: "Overview of the Gogs API including authentication, pagination, and schema"
---
The Gogs API provides a RESTful interface for interacting with your Gogs instance programmatically. It aims to follow a format similar to the [GitHub REST API v3](https://developer.github.com/v3/).
<Info>
The API is bundled with every Gogs installation. No additional setup is required.
</Info>
<Warning>
The API is still in its early stages. Content and endpoints are subject to change.
</Warning>
## Current version
All Gogs APIs are under **v1** using the request path prefix `/api/v1`.
```
https://gogs.example.com/api/v1
```
## Schema
All data is sent and received as **JSON** unless specified otherwise.
```http
HTTP/2 200
Content-Type: application/json; charset=UTF-8
```
All timestamps are returned in **RFC 3339** format:
```
YYYY-MM-DDTHH:MM:SSZ
2006-01-02T15:04:05Z07:00
```
## Authentication
There are two ways to authenticate through the Gogs API. Requests that require authentication will return `404 Not Found` instead of `403 Forbidden` in some places. This is to prevent the accidental leakage of private resources to unauthorized users.
<Tabs>
<Tab title="Basic authentication">
Basic authentication is used to obtain access tokens. Supply your username (you will be prompted for your password):
```bash
curl -u "alice" https://gogs.example.com/api/v1/users/alice/tokens
```
<Warning>
Basic authentication should only be used to generate access tokens. Do not use it for regular API requests.
</Warning>
</Tab>
<Tab title="Access token">
Personal access tokens must be sent via the `Authorization` request header.
```bash
curl -H "Authorization: token {YOUR_ACCESS_TOKEN}" https://gogs.example.com/api/v1/user/repos
```
</Tab>
</Tabs>
## Pagination
API responses that return multiple items are paginated. You can specify further pages with the `?page` query parameter.
```bash
curl https://gogs.example.com/api/v1/repos/alice/hello/issues?page=1
```
Page numbering is **1-based**. Omitting the `?page` parameter returns the first page.
### Link header
Pagination info is included in the [Link header](http://tools.ietf.org/html/rfc5988) of each response. Use this to navigate between pages programmatically.
```http
Link: <https://gogs.example.com/api/v1/repos/alice/hello/issues?page=3>; rel="next",
<https://gogs.example.com/api/v1/repos/alice/hello/issues?page=50>; rel="last"
```
The possible `rel` values are:
| Name | Description |
|---|---|
| `next` | The link relation for the immediate next page of results. |
| `last` | The link relation for the last page of results. |
| `first` | The link relation for the first page of results. |
| `prev` | The link relation for the immediate previous page of results. |
<Tip>
Always use the Link header values to navigate between pages rather than constructing URLs manually.
</Tip>
## SDKs
The following best-effort-maintained SDKs are available:
| Language | Repository |
|---|---|
| Go | [gogs/go-gogs-client](https://github.com/gogs/go-gogs-client) |
@@ -0,0 +1,4 @@
---
title: "Add labels to an issue"
openapi: "POST /repos/{owner}/{repo}/issues/{index}/labels"
---
@@ -0,0 +1,4 @@
---
title: "Create a comment"
openapi: "POST /repos/{owner}/{repo}/issues/{index}/comments"
---
@@ -0,0 +1,4 @@
---
title: "Create a label"
openapi: "POST /repos/{owner}/{repo}/labels"
---
@@ -0,0 +1,4 @@
---
title: "Create a milestone"
openapi: "POST /repos/{owner}/{repo}/milestones"
---
@@ -0,0 +1,4 @@
---
title: "Create an issue"
openapi: "POST /repos/{owner}/{repo}/issues"
---
@@ -0,0 +1,4 @@
---
title: "Delete a comment"
openapi: "DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Delete a label"
openapi: "DELETE /repos/{owner}/{repo}/labels/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Delete a milestone"
openapi: "DELETE /repos/{owner}/{repo}/milestones/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Edit a comment"
openapi: "PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Edit a milestone"
openapi: "PATCH /repos/{owner}/{repo}/milestones/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Edit an issue"
openapi: "PATCH /repos/{owner}/{repo}/issues/{index}"
---
@@ -0,0 +1,4 @@
---
title: "Get a single issue"
openapi: "GET /repos/{owner}/{repo}/issues/{index}"
---
@@ -0,0 +1,4 @@
---
title: "Get a single label"
openapi: "GET /repos/{owner}/{repo}/labels/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Get a single milestone"
openapi: "GET /repos/{owner}/{repo}/milestones/{id}"
---
@@ -0,0 +1,4 @@
---
title: "List all labels for a repository"
openapi: "GET /repos/{owner}/{repo}/labels"
---
@@ -0,0 +1,4 @@
---
title: "List comments in a repository"
openapi: "GET /repos/{owner}/{repo}/issues/comments"
---
@@ -0,0 +1,4 @@
---
title: "List comments on an issue"
openapi: "GET /repos/{owner}/{repo}/issues/{index}/comments"
---
@@ -0,0 +1,4 @@
---
title: "List issues for a repository"
openapi: "GET /repos/{owner}/{repo}/issues"
---
@@ -0,0 +1,4 @@
---
title: "List labels on an issue"
openapi: "GET /repos/{owner}/{repo}/issues/{index}/labels"
---
@@ -0,0 +1,4 @@
---
title: "List milestones for a repository"
openapi: "GET /repos/{owner}/{repo}/milestones"
---
@@ -0,0 +1,4 @@
---
title: "List user issues"
openapi: "GET /user/issues"
---
@@ -0,0 +1,4 @@
---
title: "Remove a label from an issue"
openapi: "DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id}"
---
@@ -0,0 +1,4 @@
---
title: "Remove all labels from an issue"
openapi: "DELETE /repos/{owner}/{repo}/issues/{index}/labels"
---

Some files were not shown because too many files have changed in this diff Show More