gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

Releases

Releases annotate git tags with a title, Markdown notes and a pre-release flag, expose source-code archives of any ref, and carry uploaded prebuilt files (APKs, JARs, installers) as release assets.

Notes, archives and tag creation need no configuration. Asset uploads add three properties and one filesystem store — see Asset storage and limits below.

Endpoints

Method Path Access
GET /repos/{owner}/{name}/releases Repository read
GET /repos/{owner}/{name}/releases/new Repository write
POST /repos/{owner}/{name}/releases Repository write
GET /repos/{owner}/{name}/releases/tag/{tag} Repository read
GET/POST /repos/{owner}/{name}/releases/tag/{tag}/edit Repository write
POST /repos/{owner}/{name}/releases/tag/{tag}/delete Repository write
GET /repos/{owner}/{name}/archive/{ref}.zip Repository read
GET /repos/{owner}/{name}/archive/{ref}.tar.gz Repository read
GET /api/v1/repos/{owner}/{name}/releases Repository read (token optional)
POST /api/v1/repos/{owner}/{name}/releases Token + repository write
GET /api/v1/repos/{owner}/{name}/releases/latest Repository read
GET/PATCH/DELETE /api/v1/repos/{owner}/{name}/releases/tags/{tag} Read / token + write
POST /repos/{owner}/{name}/releases/tag/{tag}/assets Repository write (multipart, field asset)
GET /repos/{owner}/{name}/releases/assets/{id}/{fileName} Repository read
POST /repos/{owner}/{name}/releases/assets/{id}/delete Repository write
GET /api/v1/repos/{owner}/{name}/releases/tags/{tag}/assets Repository read
POST /api/v1/repos/{owner}/{name}/releases/tags/{tag}/assets Token + write (multipart, field attachment)
GET/DELETE /api/v1/repos/{owner}/{name}/releases/tags/{tag}/assets/{fileName} Read / token + write

Write access follows the single access policy: personal-repo owner and collaborators, organisation MEMBER and above. A private repository's release and archive endpoints answer 404 for anyone without read access, so they never confirm the repository exists.

Archives

GET …/archive/{ref}.zip|.tar.gz streams the tree of any ref (tag, branch or commit id) straight out of the object database:

  • Entries are nested under a single <repo>-<ref>/ directory; slashes in the ref collapse to dashes.
  • Both formats are produced by the JDK (ZipOutputStream, ustar + GZIPOutputStream) — no extra archiving dependency, and no temporary files on disk.
  • Blobs are copied through, never buffered whole, so a large tree does not scale memory with repository size. There is no size cap and no rate limit — a public instance serving very large repositories should rate-limit this path at the reverse proxy, the same way a clone would be.
  • Submodules are skipped (they have no content here); symlinks become regular files holding their target path.

Responses carry Content-Disposition: attachment and application/zip / application/gzip. They are generated per request and are not cached.

Asset storage and limits

Variable Default Purpose
GITSHARK_RELEASE_ASSET_ROOT data/release-assets Directory holding the uploaded asset files
GITSHARK_MAX_ASSET_SIZE 200M Largest single asset upload; a larger file is rejected with 400
GITSHARK_MAX_BODY_SIZE 210M Cap on any HTTP request body (quarkus.http.limits.max-body-size)

GITSHARK_MAX_BODY_SIZE must stay above GITSHARK_MAX_ASSET_SIZE: the request also carries the multipart envelope, and the body cap is enforced first — a request over it is rejected at the HTTP layer with 413, before the application can produce a readable error. The 10 MB headroom in the defaults is there for that reason. Raising the asset limit means raising both together:

      GITSHARK_MAX_ASSET_SIZE: 1G
      GITSHARK_MAX_BODY_SIZE: 1100M

Note that the body cap replaces Quarkus's own default of 10 MB for every request body on the instance. The two uploads that read a body into memory (avatars, repository images) check the upload's reported length against their own 2 MB limit before reading it (ImageValidation.validateSize), so the higher cap does not turn them into a memory-exhaustion vector.

Asset bytes are written to GITSHARK_RELEASE_ASSET_ROOT in a flat directory, one file per asset, named by the asset's UUID. The uploader's file name is validated against [A-Za-z0-9][A-Za-z0-9._+~-]{0,254}, stored in the database, and used only for the Content-Disposition header — it never reaches a filesystem path, so no upload can escape the store.

The web download addresses an asset by its UUID; the REST API addresses it by file name within its release, because the numeric id in the JSON is a one-way surrogate (see GiteaIds) that nothing can be looked up by. The real id is also on the wire as uuid, and browser_download_url is the ready-made web link.

Uploads are streamed: Quarkus buffers the multipart body to a temporary file and git-shark copies it into the store, so a large asset costs disk and not heap. Plan the volume for the sum of everything users attach — unlike git objects, release assets do not deduplicate. Downloads are always sent as Content-Disposition: attachment with X-Content-Type-Options: nosniff, so an uploaded file's content type can never be rendered on the instance's origin, and each download costs one indexed UPDATE on its counter.

This directory must be on a persistent volume; it is part of the inventory in Persistent data.

Tag creation

Publishing a release whose tag does not exist yet creates an annotated tag in-core against the bare repository, authored by the publishing user (display name + account email, falling back to <username>@localhost when the account has no address). This is a real ref write in GITSHARK_STORAGE_ROOT, so it shows up for every clone and push mirror afterwards. Existing tags are never moved or rewritten, and deleting a release never deletes its tag.

Database

Two tables, added in V32__releases.sql and V33__release_assets.sql:

Table Columns Notes
releases id, repository_id, author_id, tag_name, title, body, commit_id, prerelease, created_at unique (repository_id, tag_name) — one release per tag; ON DELETE CASCADE from both repositories and users
release_assets id, release_id, uploader_id, file_name, content_type, size_bytes, download_count, uploaded_at unique (release_id, file_name); ON DELETE CASCADE from releases and users. Asset bytes are not here — they are on the filesystem, named by id

Release notes live in the database; the tag and its objects live in the git repository; asset bytes live in GITSHARK_RELEASE_ASSET_ROOT. A restore therefore needs all three stores in sync — see Persistent data.

Deleting an asset, a release, or a whole repository removes the stored files in the same operation. There is no reconciliation job, and rows and files can drift apart in both directions if a process is killed mid-transaction:

  • A row without its file. Every delete path runs inside one transaction with the file removal before the commit, so a crash in that window rolls the row back while the file is already gone. The asset stays listed and its download answers 404. Find these by checking each id from release_assets against the asset root.
  • A file without its row. An upload persists the row, copies the bytes, then commits; a failure after the copy rolls the row back and leaves the file. Find these by checking each file name in the asset root against select id from release_assets;.

Both need a literal process kill in a narrow window, so neither is expected in normal operation — but only the second one wastes space silently; the first one is visible to users as a broken download.

Troubleshooting

Symptom Cause
400 "does not exist — pick a target to create it from" The tag is unknown and no target branch was submitted.
400 "already has a release" One release per tag; edit the existing one instead.
400 "is not a valid tag name" git ref rules (no spaces, no .., no trailing /).
Archive returns 404 The ref does not resolve, or the suffix is neither .zip nor .tar.gz.
Archive download stalls on a huge repository Expected — it streams; put a proxy timeout/rate limit in front if this is abused.
Upload fails with 413 and no error page Over GITSHARK_MAX_BODY_SIZE, which is enforced before the application sees the request. Raise both size variables.
400 "File is too large" Under the body cap but over GITSHARK_MAX_ASSET_SIZE.
400 "Use a file name of letters, digits, …" The asset name has a space or a directory in it; the user must rename before uploading.
400 "already has a file named …" One file name per release; delete the old one first.
Asset download returns 404 although the page lists the file The asset root is not the directory it was at upload time (missing or changed volume mount). Check GITSHARK_RELEASE_ASSET_ROOT against the actual mount.
The asset root grows unexpectedly Nothing prunes it. Compare the total against select sum(size_bytes) from release_assets; — a large gap means orphaned files from interrupted uploads.

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog