Compare commits

...

45 commits

Author SHA1 Message Date
Matthias Riße
6c61f80d21 feat: copy annexed files on pull request merge (#62)
Some checks failed
/ build-oci-image (rootful) (push) Has been cancelled
/ build-oci-image (rootless) (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled
This changes the PR merge process such that annexed files contained in the to-be-merged commits are copied from the head repository to the base repository as part of the merge, similar to how it is done for LFS files.

Fixes #11.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [ ] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/62
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-21 21:31:48 +00:00
Matthias Riße
1298a315bd fix: improve git-annex UUID cache update times (#65)
Some checks are pending
/ build-oci-image (rootful) (push) Waiting to run
/ build-oci-image (rootless) (push) Waiting to run
/ release (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
Globbing for the config files is marginally faster than walking the
directory and checking for config files. Replacing the expensive calls
to `git config` for each repository with reading the repository's config
as an ini file is two orders of magnitude faster.

All in all this reduces the required time initializing the cache for
approx. 3000 repositories from approx. 5s to 50ms.

The server startup now also logs how long the cache update took and the
cache update is only done if p2phttp support is not disabled, because
p2phttp support is currently the only feature that requires the UUID
cache.

Fixes #63, fixes #64.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/65
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-21 13:59:06 +00:00
Matthias Riße
c5b3812553 Use annexed content for comparison in diffs (#57)
Some checks failed
/ build-oci-image (rootful) (push) Has been cancelled
/ build-oci-image (rootless) (push) Has been cancelled
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled
This makes it such that annexed files are treated like plain git files
in comparisons (e.g. the diff of a commit).

It also changes the image diff viewer to show a more reasonable error
message when one of the annexed files under comparison is missing.

Fixes #56.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/57
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
0ee62840ff Invalidate outdated annex UUID cache entries (#60)
The previous implementation could lead to errors e.g. when a repository
was removed and a new one with a new UUID was created under the same
name. This now checks the validity of the retrieved cache entry every
time and invalidates the cache if necessary.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/60
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
efe77599e6 Pre-populate the git-annex UUID cache at startup (#59)
This eliminates the wait time for the first p2phttp connection since
server startup at the cost of adding that time to the startup itself.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/59
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
22855c6238 Cache git-annex UUID to repository path mappings (#54)
Always walking the filesystem and searching for UUIDs slowed p2phttp
operations down significantly on a production server with more than a
handful of repositories. This caching strategy ensures that only the
first call is rather slow, and subsequent ones should be much faster.

This should better be implemented as a background job, but for now this
is a simple solution to the problem.

Fixes #53.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/54
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
aa683cb1e2 Explicitly set http(s) default ports in annex.url (#55)
Otherwise, git-annex tries to use its own default port (9417) and fails.

Fixes #52.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/55
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
99d2de8956 Fix Forgejo version in published OCI images (#51)
The Forgejo version is derived from the git history, so the image build
needs to happen in the context of a full repository clone.

Also, the post-processing of the version string needs to remove the
second occurrence of "-g", as the first one is now part of the added
"-git-annex" part.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/51
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
4b75d40853 Add an OCI image build and publish workflow (#50)
Fixes #49.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/50
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
aa6674de6e Simplify git blob to annex key lookup (#43)
Fixes #27.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/43
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:15 +01:00
Matthias Riße
971f6f7bab Add git-annex p2phttp support (#42)
This adds a new endpoint under `/git-annex-p2phttp` which acts as an
authenticating proxy to git-annex' p2phttp server. This makes it
possible to set `annex+<server-url>/git-annex-p2phttp` as
`remote.<name>.annexurl` and use git-annex fully over http(s) with the
normal credentials and access tokens provided by Forgejo.

Fixes #25.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/42
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
1dd3a341a7 Add tests for git annex drop (#47)
This adds some rudimentary tests that drop files in a repository's clone
as well as from a repository on Forgejo.

Fixes #4.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/47
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
592b23f5cd Add git-annex' testremote to the test suite (#48)
The `git annex testremote` command runs a built-in set of tests against
a remote. It cannot hurt to check our implementation of a git-annex
remote against it too.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/48
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
4d65d593ad Only upload to annex in doAnnexUploadTest (#46)
The previous implementation both uploaded to the annex and pushed to the
git repository. This meant that the tests checking that uploads without
permission fail actually could pass when the git push failed but the
git-annex upload didn't. The tests didn't catch the situation where
unauthorized users could modify the annex.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/46
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
3cf2111dbf Use PATH when looking for git commands (#44)
This fixes some issues when using a git-annex that is installed in a
different location than where git is installed, e.g. when using the
git-annex-standalone release or one installed with nix.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/44
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
cdc80b9fe6 Allow anonymous HEAD requests to annex/objects (#41)
Git-annex emits HEAD requests for keys while trying to drop them from a
repository that was cloned via http. Forgejo asked for authentication
for these HEAD requests. This meant that cloning and getting files was
possible without authentication, but dropping was not.

Since the response to a HEAD request is a subset of the response to a
GET request it is safe to make those unauthenticated as well. That is
what this change does, although limited to the
:username/:reponame/annex/objects endpoint.

Fixes #40.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/41
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Michael Hanke
8065657151 Elevate external markup renderer interface for annexed file content (#36)
Previously, an external renderer that matched on an annexed file would
only see its content streamed via `STDIN`, or a temporary file with a copy
of its content would be generated and passed-by-filepath (with
`IS_INPUT_FILE=true`). Whether that happens, is also subject to
`MAX_DISPLAY_FILE_SIZE` (which defaults to 8MB).

This was problematic, because annexed files tend to be large. Moreover,
if present, they already exist as write-protected files on the
file-system. Creating a copy is both expensive and serves no particular
purpose.

This commit changes how external renderers are called.

1) With `IS_INPUT_FILE=true`, the renderer is passed the true location
   of an annex key, if present, and an empty path, if not.
2) The original, repository-relative path of the rendering target is
   made available to the renderer via the `GITEA_RELATIVE_PATH`
   environment variable.

To achieve a lean implementation, the `Blob` of the rendering target
is passed on to the `RenderContext` (because the implementation of
the annex-related functionality is centered on this dtype.

This change makes it less costly to increase `MAX_DISPLAY_FILE_SIZE`,
in order to make large, annexed files eligible for markup rendering,
because no content copies will be made any longer.

External renderers can now use the original file path, with the full
original filename, including extensions, for decision making. For
example, to detect particular compression formats based in a file name
extension, or to alter the rendering based on contextual information
encoded in the file path (e.g., a multi-file data structure with a
particular organization pattern).

Apart from the additional environment variable, there is no change to
the handling of renderers that take their input via `STDIN` (i.e.,
`IS_INPUT_FILE=false`).

Fixes #35.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/36
Reviewed-by: matrss <matrss@noreply.codeberg.org>
Co-authored-by: Michael Hanke <michael.hanke@gmail.com>
Co-committed-by: Michael Hanke <michael.hanke@gmail.com>
2025-02-08 16:52:14 +01:00
Matthias Riße
78ff09f63d Change the icon for annexed files to file-binary (#29)
Fixes #26.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/29
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
4c60dfb6f0 Improve views for annexed but missing files (#28)
Previously, trying to view files that were annexed, but missing, just
led to an uninformative error 500. This was rather confusing.

With these changes it now shows the pointer target instead of the
(missing) content of the file, and also indicates this situation in the
"stored with git-annex" message. For semantic correctness views for
missing files return a 404 instead of a 200, as they would with the
content present.

Fixes #7, fixes #13.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/28
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Michael Hanke
5b2ae61a12 Add git-annex also to the rootless container (#24)
Same as 89f8aa0bf5, but for the rootless container.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/24
Reviewed-by: matrss <matrss@noreply.codeberg.org>
Co-authored-by: Michael Hanke <michael.hanke@gmail.com>
Co-committed-by: Michael Hanke <michael.hanke@gmail.com>
2025-02-08 16:52:14 +01:00
Matthias Riße
e83f575da5 Git-annex web uploads (#21)
This implements support for uploading files into the annex using the web
interface.

If a repository is a git-annex-enabled repository all files will be
added to it using git annex add. This means that the repository's
configuration for what to put into the annex (annex.largefiles in
gitattributes) will be respected.

Plain git repositories without git-annex will work as before, directly
uploading to git.

Fixes #5.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/21
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
246131cc69 Error if git-annex is enabled but missing (#16)
Copied from https://github.com/neuropoly/gitea/pull/47

This adds a check so that if `setting.Annex.Enabled` is true and git-annex is not in the PATH Forgejo will abort on startup with a reasonable error message.

Fixes #15.

Reviewed-on: https://codeberg.org/matrss/forgejo-aneksajo/pulls/16
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:14 +01:00
Matthias Riße
e97e20427d Add git-annex to docker image 2025-02-08 16:52:14 +01:00
Matthias Riße
5ff7951560 Adapt patch to upstream changes
Test with different objectFormats.
2025-02-08 16:52:14 +01:00
Nick
56a9ea1fc3 git-annex: views for annex files
This updates the repo index/file view endpoints so annex files match the way
LFS files are rendered, making annexed files accessible via the web instead of
being black boxes only accessible by git clone.

This mostly just duplicates the existing LFS logic. It doesn't try to combine itself
with the existing logic, to make merging with upstream easier. If upstream ever
decides to accept, I would like to try to merge the redundant logic.

The one bit that doesn't directly copy LFS is my choice to hide annex-symlinks.
LFS files are always _pointer files_ and therefore always render with the "file"
icon and no special label, but annex files come in two flavours: symlinks or
pointer files. I've conflated both kinds to try to give a consistent experience.

The tests in here ensure the correct download link (/media, from the last PR)
renders in both the toolbar and, if a binary file (like most annexed files will be),
in the main pane, but it also adds quite a bit of code to make sure text files
that happen to be annexed are dug out and rendered inline like LFS files are.
2025-02-08 16:52:14 +01:00
Matthias Riße
82c9a86e83 Adapt patch to upstream changes
Use tests.FileCmp instead of util.FileCmp.
2025-02-08 16:52:14 +01:00
Matthias Riße
2845a75cbe Adapt patch to upstream changes
Test with different objectFormats.
2025-02-08 16:52:14 +01:00
Nick
2c0cb04776 git-annex: make /media/ download annexed content
Previously, Gitea's LFS support allowed direct-downloads of LFS content,
via http://$HOSTNAME:$PORT/$USER/$REPO/media/branch/$BRANCH/$FILE
Expand that grace to git-annex too. Now /media should provide the
relevant *content* from the .git/annex/objects/ folder.

This adds tests too. And expands the tests to try symlink-based annexing,
since /media implicitly supports both that and pointer-file-based annexing.
2025-02-08 16:52:14 +01:00
Matthias Riße
635770f8a4 Adapt patch to upstream changes
The git repository must be closed after using it. Without this change
some tests started to fail due to the lingering repository running into
a timeout.
2025-02-08 16:52:14 +01:00
Nick
0831aeccd1 git-annex: create modules/annex
This moves the `annexObjectPath()` helper out of the tests and into a
dedicated sub-package as `annex.ContentLocation()`, and expands it with
`.Pointer()` (which validates using `git annex examinekey`),
`.IsAnnexed()` and `.Content()` to make it a more useful module.

The tests retain their own wrapper version of `ContentLocation()`
because I tried to follow close to the API modules/lfs uses, which in
terms of abstract `git.Blob` and `git.TreeEntry` objects, not in terms
of `repoPath string`s which are more convenient for the tests.
2025-02-08 16:52:14 +01:00
Matthias Riße
0c81519d4e Adapt patch to upstream changes
The "context_service" import was changed to use the default name of just
"context". The patch set had to be adapted for that.
2025-02-08 16:52:14 +01:00
Matthias Riße
3c11eb1e26 Adapt patch to upstream changes
Usage of `path` was replaced by `path/filepath` in upstream forgejo, and
it made sense to use that as well where `path` was previously used. The
`setHeaderCacheForever` function and the `sendFile` method had their
signature changed.
2025-02-08 16:52:14 +01:00
Matthias Riße
24264cb45f Fix exit code check for git command
The err.IsExitCode method was changed to a function IsErrorExitCode
taking err as its first argument in
1e7a6483b8.
2025-02-08 16:52:14 +01:00
Matthias Riße
f0da2f268a Replace m.GetOptions with m.Methods
This applies the same changes that were done in
265cd70bdb to the git-annex specific
routes as well.
2025-02-08 16:52:13 +01:00
Nick
d8bf745749 git-annex: support downloading over HTTP
This makes HTTP symmetric with SSH clone URLs.

This gives us the fancy feature of _anonymous_ downloads,
so people can access datasets without having to set up an
account or manage ssh keys.

Previously, to access "open access" data shared this way,
users would need to:

  1. Create an account on gitea.example.com
  2. Create ssh keys
  3. Upload ssh keys (and make sure to find and upload the correct file)
  4. `git clone git@gitea.example.com:user/dataset.git`
  5. `cd dataset`
  6. `git annex get`

This cuts that down to just the last three steps:

  1. `git clone https://gitea.example.com/user/dataset.git`
  2. `cd dataset`
  3. `git annex get`

This is significantly simpler for downstream users, especially for those
unfamiliar with the command line.

Unfortunately there's no uploading. While git-annex supports uploading
over HTTP to S3 and some other special remotes, it seems to fail on a
_plain_ HTTP remote. See https://github.com/neuropoly/gitea/issues/7
and https://git-annex.branchable.com/forum/HTTP_uploads/#comment-ce28adc128fdefe4c4c49628174d9b92.

This is not a major loss since no one wants uploading to be anonymous anyway.

To support private repos, I had to hunt down and patch a secret extra security
corner that Gitea only applies to HTTP for some reason (services/auth/basic.go).

This was guided by https://git-annex.branchable.com/tips/setup_a_public_repository_on_a_web_site/

Fixes https://github.com/neuropoly/gitea/issues/3

Co-authored-by: Mathieu Guay-Paquet <mathieu.guaypaquet@polymtl.ca>
2025-02-08 16:52:13 +01:00
Nick
f42432a9e4 git-annex: add configuration setting [annex].ENABLED
Fixes https://github.com/neuropoly/gitea/issues/8

Co-authored-by: Mathieu Guay-Paquet <mathieu.guaypaquet@gmail.com>
2025-02-08 16:52:13 +01:00
Matthias Riße
cc193ec4ef Fix failing tests
Multiple tests that worked fine on v1.20.4-1 started to fail after the
rebase onto v1.20.5-1. These tests are:
- TestGitAnnexPermissions/Private/Owner/HTTP/Init
- TestGitAnnexPermissions/Private/Owner/HTTP/Download
- TestGitAnnexPermissions/Private/Writer/HTTP/Init
- TestGitAnnexPermissions/Private/Writer/HTTP/Download
- TestGitAnnexPermissions/Private/Reader/HTTP/Init
- TestGitAnnexPermissions/Private/Reader/HTTP/Download

What these tests have in common is that they all operate on a private
repository via http with authentication.

They broke at some point between v1.20.4-1 and v1.20.5-1, so I did a
bisect between these two points running the offending tests. This
brought me to the conclusion that
ee48c0d5ea introduced the issue.

The thing is, this commit does not change any code, it only changes the
test environment. Among other things that didn't look as suspicious, it
changes the container image from a bespoke test_env image based on
debian bullseye to a node image based on debian bookworm. Obviously,
this means that there are many version differences between the two.

The first one I looked at was git. The previous bullseye image used a
manually installed git version 2.40.0, while the bookworm image has
2.39.2 installed. Updating git in the new image did not fix the issue,
however.

The next thing I looked at was the git-annex version. Bullseye had
8.20210223 installed and worked, while bookworm used 10.20230126 when
the tests broke. So I tried my luck upgrading to a more recent version
via neurodebian (10.20240227-1~ndall+1). This still worked fine on
bullseye and now also works fine on bookworm.

I have no idea why this specific version of git-annex broke the tests,
but at least there was a commit to pinpoint this to, which isn't always
the case with docker images silently changing beneath you...

Below are the versions as they are reported by git and git-annex:

bullseye (works):

    git version 2.30.2
    git-annex version: 8.20210223
    build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Feeds Testsuite S3 WebDAV
    dependency versions: aws-0.22 bloomfilter-2.0.1.0 cryptonite-0.26 DAV-1.3.4 feed-1.3.0.1 ghc-8.8.4 http-client-0.6.4.1 persistent-sqlite-2.10.6.2 torrent-10000.1.1 uuid-1.3.13 yesod-1.6.1.0
    key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
    remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
    operating system: linux x86_64
    supported repository versions: 8
    upgrade supported from repository versions: 0 1 2 3 4 5 6 7

bullseye + git-annex from neurodebian (works):

    git version 2.30.2
    git-annex version: 10.20240227-1~ndall+1
    build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
    dependency versions: aws-0.22.1 bloomfilter-2.0.1.0 cryptonite-0.29 DAV-1.3.4 feed-1.3.2.1 ghc-9.0.2 http-client-0.7.13.1 persistent-sqlite-2.13.1.0 torrent-10000.1.1 uuid-1.3.15 yesod-1.6.2.1
    key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
    remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
    operating system: linux x86_64
    supported repository versions: 8 9 10
    upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10

bookworm (fails):

    git version 2.39.2
    git-annex version: 10.20230126
    build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
    dependency versions: aws-0.22.1 bloomfilter-2.0.1.0 cryptonite-0.29 DAV-1.3.4 feed-1.3.2.1 ghc-9.0.2 http-client-0.7.13.1 persistent-sqlite-2.13.1.0 torrent-10000.1.1 uuid-1.3.15 yesod-1.6.2.1
    key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
    remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
    operating system: linux x86_64
    supported repository versions: 8 9 10
    upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10

bookworm + git-annex from neurodebian (works):

    git version 2.39.2
    git-annex version: 10.20240227-1~ndall+1
    build flags: Assistant Webapp Pairing Inotify DBus DesktopNotify TorrentParser MagicMime Benchmark Feeds Testsuite S3 WebDAV
    dependency versions: aws-0.22.1 bloomfilter-2.0.1.0 cryptonite-0.29 DAV-1.3.4 feed-1.3.2.1 ghc-9.0.2 http-client-0.7.13.1 persistent-sqlite-2.13.1.0 torrent-10000.1.1 uuid-1.3.15 yesod-1.6.2.1
    key/value backends: SHA256E SHA256 SHA512E SHA512 SHA224E SHA224 SHA384E SHA384 SHA3_256E SHA3_256 SHA3_512E SHA3_512 SHA3_224E SHA3_224 SHA3_384E SHA3_384 SKEIN256E SKEIN256 SKEIN512E SKEIN512 BLAKE2B256E BLAKE2B256 BLAKE2B512E BLAKE2B512 BLAKE2B160E BLAKE2B160 BLAKE2B224E BLAKE2B224 BLAKE2B384E BLAKE2B384 BLAKE2BP512E BLAKE2BP512 BLAKE2S256E BLAKE2S256 BLAKE2S160E BLAKE2S160 BLAKE2S224E BLAKE2S224 BLAKE2SP256E BLAKE2SP256 BLAKE2SP224E BLAKE2SP224 SHA1E SHA1 MD5E MD5 WORM URL X*
    remote types: git gcrypt p2p S3 bup directory rsync web bittorrent webdav adb tahoe glacier ddar git-lfs httpalso borg hook external
    operating system: linux x86_64
    supported repository versions: 8 9 10
    upgrade supported from repository versions: 0 1 2 3 4 5 6 7 8 9 10
2025-02-08 16:52:13 +01:00
Matthias Riße
3d7458b8cc Install git-annex in the testing workflow 2025-02-08 16:52:13 +01:00
Matthias Riße
37a8fa9b5c Adapt patch to upstream changes 2025-02-08 16:52:13 +01:00
Matthias Riße
d13faeccaa Adapt patch to upstream changes
A dead code check started to complain because FileCmp was only used in
tests. Moved the function to test_utils.
2025-02-08 16:52:13 +01:00
Matthias Riße
57de00e5b9 Adapt patch to upstream changes
Repository creation now expects an objectFormat to be specified for git.
2025-02-08 16:52:13 +01:00
Nick
52485edf64 git-annex tests
Fixes https://github.com/neuropoly/gitea/issues/11

Tests:

* `git annex init`
* `git annex copy --from origin`
* `git annex copy --to origin`

over:

* ssh

for:

* the owner
* a collaborator
* a read-only collaborator
* a stranger

in a

* public repo
* private repo

And then confirms:

* Deletion of the remote repo (to ensure lockdown isn't messing with us: https://git-annex.branchable.com/internals/lockdown/#comment-0cc5225dc5abe8eddeb843bfd2fdc382)

------

To support all this:

* Add util.FileCmp()
* Patch withKeyFile() so it can be nested in other copies of itself

-------

Many thanks to Mathieu for giving style tips and catching several bugs,
including a subtle one in util.filecmp() which neutered it.

Co-authored-by: Mathieu Guay-Paquet <mathieu.guay-paquet@polymtl.ca>
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
2025-02-08 16:52:13 +01:00
Nick Guenther
1d01b58227 git-annex support
[git-annex](https://git-annex.branchable.com/) is a more complicated cousin to
git-lfs, storing large files in an optional-download side content.  Unlike lfs,
it allows mixing and matching storage remotes, so the content remote(s) doesn't
need to be on the same server as the git remote, making it feasible to scatter
a collection across cloud storage, old harddrives, or anywhere else storage can
be scavenged.  Since this can get complicated, fast, it has a content-tracking
database (`git annex whereis`) to help find everything later.

The use-case we imagine for including it in Gitea is just the simple case, where
we're primarily emulating git-lfs: each repo has its large content at the same URL.

Our motivation is so we can self-host https://www.datalad.org/ datasets, which
currently are only hostable by fragilely scrounging together cloud storage --
and having to manage all the credentials associated with all the pieces -- or at
https://openneuro.org which is fragile in its own ways.

Supporting git-annex also allows multiple Gitea instance to be annex remotes for
each other, mirroring the content or otherwise collaborating the split up the
hosting costs.

Enabling
--------

TODO

HTTP
----

TODO

Permission Checking
-------------------

This tweaks the API in routers/private/serv.go to expose the calling user's
computed permission, instead of just returning HTTP 403.

This doesn't fit in super well. It's the opposite from how the git-lfs support is
done, where there's a complete list of possible subcommands and their matching
permission levels, and then the API compares the requested with the actual level
and returns HTTP 403 if the check fails.

But it's necessary. The main git-annex verbs, 'git-annex-shell configlist' and
'git-annex-shell p2pstdio' are both either read-only or read-write operations,
depending on the state on disk on either end of the connection and what the user
asked it to ask for, with no way to know before git-annex examines the situation.
So tell the level via GIT_ANNEX_READONLY and trust it to handle itself.

In the older Gogs version, the permission was directly read in cmd/serv.go:

```
mode, err = db.UserAccessMode(user.ID, repo)
```
- 966e925cf3/internal/cmd/serv.go (L334)

but in Gitea permission enforcement has been centralized in the API layer.
(perhaps so the cmd layer can avoid making direct DB connections?)

Deletion
--------

git-annex has this "lockdown" feature where it tries
really quite very hard to prevent you deleting its
data, to the point that even an rm -rf won't do it:
each file in annex/objects/ is nested inside a
folder with read-only permissions.

The recommended workaround is to run chmod -R +w when
you're sure you actually want to delete a repo. See
https://git-annex.branchable.com/internals/lockdown

So we edit util.RemoveAll() to do just that, so now
it's `chmod -R +w && rm -rf` instead of just `rm -rf`.
2025-02-08 16:52:13 +01:00
Matthias Riße
2771c2e591 Fix name in package-lock.json
The frontend-checks job started failing because the declared name does
not match the repository name.
2025-02-08 16:52:13 +01:00
Matthias Riße
3c7f598230 Run testing workflow unconditionally 2025-02-08 16:52:13 +01:00
72 changed files with 4309 additions and 66 deletions

View file

@ -13,6 +13,8 @@ runs:
run: |
export DEBIAN_FRONTEND=noninteractive
echo "deb http://deb.debian.org/debian/ ${RELEASE} main" > "/etc/apt/sources.list.d/${RELEASE}.list"
wget -O- http://neuro.debian.net/lists/bookworm.de-fzj.libre | tee /etc/apt/sources.list.d/neurodebian.sources.list
apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
env:
RELEASE: ${{inputs.release}}
- name: install packages
@ -24,6 +26,7 @@ runs:
- name: remove temporary package list to prevent using it in other steps
run: |
rm "/etc/apt/sources.list.d/${RELEASE}.list"
rm "/etc/apt/sources.list.d/neurodebian.sources.list"
apt-get update -qq
env:
RELEASE: ${{inputs.release}}

View file

@ -0,0 +1,41 @@
on:
push:
branches:
- 'forgejo'
tags:
- '*-git-annex*'
jobs:
build-oci-image:
runs-on: docker
strategy:
matrix:
type: ["rootful", "rootless"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # fetch the full history so that the Forgejo version is determined properly
- name: Determine registry and username
id: determine-registry-and-username
run: |
echo "registry=${GITHUB_SERVER_URL#https://}" >> "$GITHUB_OUTPUT"
echo "username=${GITHUB_REPOSITORY%/*}" >> "$GITHUB_OUTPUT"
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ steps.determine-registry-and-username.outputs.registry }}
username: ${{ steps.determine-registry-and-username.outputs.username }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ${{ (matrix.type == 'rootful' && 'Dockerfile') || (matrix.type == 'rootless' && 'Dockerfile.rootless') }}
push: true
tags: ${{ steps.determine-registry-and-username.outputs.registry }}/${{ github.repository }}:${{ github.ref_name }}${{ (matrix.type == 'rootful' && ' ') || (matrix.type == 'rootless' && '-rootless') }}

View file

@ -10,7 +10,6 @@ on:
jobs:
backend-checks:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:20-bookworm'
@ -27,7 +26,6 @@ jobs:
- run: su forgejo -c 'make --always-make -j$(nproc) lint-backend tidy-check swagger-check fmt-check swagger-validate' # ensure the "go-licenses" make target runs
- uses: ./.forgejo/workflows-composite/build-backend
frontend-checks:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:20-bookworm'
@ -176,7 +174,6 @@ jobs:
TAGS: bindata
TEST_REDIS_SERVER: cacher:${{ matrix.cacher.port }}
test-mysql:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
needs: [backend-checks, frontend-checks]
container:
@ -199,15 +196,13 @@ jobs:
- name: install dependencies & git >= 2.42
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git git-lfs
packages: git git-annex-standalone git-lfs
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-mysql-migration test-mysql'
timeout-minutes: 120
env:
USE_REPO_TEST_DIR: 1
test-pgsql:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
needs: [backend-checks, frontend-checks]
container:
@ -236,17 +231,15 @@ jobs:
- name: install dependencies & git >= 2.42
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git git-lfs
packages: git git-annex-standalone git-lfs
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-pgsql-migration test-pgsql'
timeout-minutes: 120
env:
RACE_ENABLED: true
USE_REPO_TEST_DIR: 1
TEST_LDAP: 1
test-sqlite:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
needs: [backend-checks, frontend-checks]
container:
@ -258,25 +251,21 @@ jobs:
- name: install dependencies & git >= 2.42
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git git-lfs
packages: git git-annex-standalone git-lfs
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-sqlite-migration test-sqlite'
timeout-minutes: 120
env:
TAGS: sqlite sqlite_unlock_notify
RACE_ENABLED: true
TEST_TAGS: sqlite sqlite_unlock_notify
USE_REPO_TEST_DIR: 1
security-check:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
needs:
- test-sqlite
- test-pgsql
- test-mysql
- test-remote-cacher
- test-unit
container:
image: 'data.forgejo.org/oci/node:20-bookworm'
options: --tmpfs /tmp:exec,noatime

View file

@ -78,6 +78,7 @@ RUN apk --no-cache add \
sqlite \
su-exec \
gnupg \
git-annex \
&& rm -rf /var/cache/apk/*
RUN addgroup \

View file

@ -71,6 +71,7 @@ RUN apk --no-cache add \
git \
curl \
gnupg \
git-annex \
&& rm -rf /var/cache/apk/*
RUN addgroup \

View file

@ -8,7 +8,7 @@ self := $(location)
@tmpdir=`mktemp --tmpdir -d` ; \
echo Using temporary directory $$tmpdir for test repositories ; \
USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \
STATUS=$$? ; rm -r "$$tmpdir" ; exit $$STATUS
STATUS=$$? ; chmod -R +w "$$tmpdir" && rm -r "$$tmpdir" ; exit $$STATUS
else
@ -104,7 +104,7 @@ else
FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY}
else
# drop the "g" prefix prepended by git describe to the commit hash
FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/')+${GITEA_COMPATIBILITY}
FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/2')+${GITEA_COMPATIBILITY}
endif
endif
FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//')

View file

@ -38,6 +38,7 @@ import (
const (
lfsAuthenticateVerb = "git-lfs-authenticate"
gitAnnexShellVerb = "git-annex-shell"
)
// CmdServ represents the available serv sub-command.
@ -79,6 +80,7 @@ var (
"git-upload-archive": perm.AccessModeRead,
"git-receive-pack": perm.AccessModeWrite,
lfsAuthenticateVerb: perm.AccessModeNone,
gitAnnexShellVerb: perm.AccessModeNone, // annex permissions are enforced by GIT_ANNEX_SHELL_READONLY, rather than the Gitea API
}
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
@ -212,6 +214,28 @@ func runServ(c *cli.Context) error {
}
}
if verb == gitAnnexShellVerb {
if !setting.Annex.Enabled {
return fail(ctx, "Unknown git command", "git-annex request over SSH denied, git-annex support is disabled")
}
if len(words) < 3 {
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
}
// git-annex always puts the repo in words[2], unlike most other
// git subcommands; and it sometimes names repos like /~/, as if
// $HOME should get expanded while also being rooted. e.g.:
// git-annex-shell 'configlist' '/~/user/repo'
// git-annex-shell 'sendkey' '/user/repo 'key'
repoPath = words[2]
repoPath = strings.TrimPrefix(repoPath, "/")
repoPath = strings.TrimPrefix(repoPath, "~/")
}
// prevent directory traversal attacks
repoPath = filepath.Clean("/" + repoPath)[1:]
rr := strings.SplitN(repoPath, "/", 2)
if len(rr) != 2 {
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
@ -225,6 +249,18 @@ func runServ(c *cli.Context) error {
// so that username and reponame are not affected.
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
// put the sanitized repoPath back into the argument list for later
if verb == gitAnnexShellVerb {
// git-annex-shell demands an absolute path
absRepoPath, err := filepath.Abs(filepath.Join(setting.RepoRootPath, repoPath))
if err != nil {
return fail(ctx, "Error locating repoPath", "%v", err)
}
words[2] = absRepoPath
} else {
words[1] = repoPath
}
if alphaDashDotPattern.MatchString(reponame) {
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
}
@ -303,21 +339,45 @@ func runServ(c *cli.Context) error {
return nil
}
var gitcmd *exec.Cmd
gitBinPath := filepath.Dir(git.GitExecutable) // e.g. /usr/bin
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
if _, err := os.Stat(gitBinVerb); err != nil {
gitBinVerb, err := exec.LookPath(verb)
if err != nil {
// if the command "git-upload-pack" doesn't exist, try to split "git-upload-pack" to use the sub-command with git
// ps: Windows only has "git.exe" in the bin path, so Windows always uses this way
// ps: git-annex-shell and other extensions may not necessarily be in gitBinPath,
// but '{gitBinPath}/git annex-shell' should be able to find them on $PATH.
verbFields := strings.SplitN(verb, "-", 2)
if len(verbFields) == 2 {
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
gitcmd = exec.CommandContext(ctx, git.GitExecutable, verbFields[1], repoPath)
gitBinVerb = git.GitExecutable
words = append([]string{verbFields[1]}, words...)
}
}
if gitcmd == nil {
// by default, use the verb (it has been checked above by allowedCommands)
gitcmd = exec.CommandContext(ctx, gitBinVerb, repoPath)
// by default, use the verb (it has been checked above by allowedCommands)
gitcmd := exec.CommandContext(ctx, gitBinVerb, words[1:]...)
if verb == gitAnnexShellVerb {
// This doesn't get its own isolated section like LFS does, because LFS
// is handled by internal Gitea routines, but git-annex has to be shelled out
// to like other git subcommands, so we need to build up gitcmd.
// TODO: does this work on Windows?
gitcmd.Env = append(gitcmd.Env,
// "If set, disallows running git-shell to handle unknown commands."
// - git-annex-shell(1)
"GIT_ANNEX_SHELL_LIMITED=True",
// "If set, git-annex-shell will refuse to run commands
// that do not operate on the specified directory."
// - git-annex-shell(1)
fmt.Sprintf("GIT_ANNEX_SHELL_DIRECTORY=%s", words[2]),
)
if results.UserMode < perm.AccessModeWrite {
// "If set, disallows any action that could modify the git-annex repository."
// - git-annex-shell(1)
// We set this when the backend API has told us that we don't have write permission to this repo.
log.Debug("Setting GIT_ANNEX_SHELL_READONLY=True")
gitcmd.Env = append(gitcmd.Env, "GIT_ANNEX_SHELL_READONLY=True")
}
}
process.SetSysProcAttribute(gitcmd)

View file

@ -9,6 +9,7 @@ import (
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
@ -247,6 +248,12 @@ func runWeb(ctx *cli.Context) error {
createPIDFile(ctx.String("pid"))
}
if setting.Annex.Enabled {
if _, err := exec.LookPath("git-annex"); err != nil {
log.Fatal("You have enabled git-annex support but git-annex is not installed. Please make sure that Forgejo's PATH contains the git-annex executable.")
}
}
if !setting.InstallLock {
if err := serveInstall(ctx); err != nil {
return err
@ -311,6 +318,10 @@ func listen(m http.Handler, handleRedirector bool) error {
log.Info("LFS server enabled")
}
if setting.Annex.Enabled {
log.Info("git-annex enabled")
}
var err error
switch setting.Protocol {
case setting.HTTP:

View file

@ -2678,6 +2678,17 @@ LEVEL = Info
;; Limit the number of concurrent upload/download operations within a batch
;BATCH_OPERATION_CONCURRENCY = 8
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[annex]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Whether git-annex is enabled; defaults to false
;ENABLED = false
;; Whether to disable p2phttp support; default is the same as repository.DISABLE_HTTP_GIT
;DISABLE_P2PHTTP = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; settings for packages, will override storage setting

256
modules/annex/annex.go Normal file
View file

@ -0,0 +1,256 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Unlike modules/lfs, which operates mainly on git.Blobs, this operates on git.TreeEntrys.
// The motivation for this is that TreeEntrys have an easy pointer to the on-disk repo path,
// while blobs do not (in fact, if building with TAGS=gogit, blobs might exist only in a mock
// filesystem, living only in process RAM). We must have the on-disk path to do anything
// useful with git-annex because all of its interesting data is on-disk under .git/annex/.
package annex
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"gopkg.in/ini.v1" //nolint:depguard // This import is forbidden in favor of using the setting module, but we need ini parsing for something other than Forgejo settings
)
// ErrBlobIsNotAnnexed occurs if a blob does not contain a valid annex key
var ErrBlobIsNotAnnexed = errors.New("not a git-annex pointer")
func PrivateInit(ctx context.Context, repoPath string) error {
if _, _, err := git.NewCommand(ctx, "config", "annex.private", "true").RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
return err
}
if _, _, err := git.NewCommand(ctx, "annex", "init").RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
return err
}
return nil
}
func LookupKey(blob *git.Blob) (string, error) {
stdout, _, err := git.NewCommand(git.DefaultContext, "annex", "lookupkey", "--ref").AddDynamicArguments(blob.ID.String()).RunStdString(&git.RunOpts{Dir: blob.Repo().Path})
if err != nil {
return "", ErrBlobIsNotAnnexed
}
key := strings.TrimSpace(stdout)
return key, nil
}
// LookupKeyBatch runs git annex lookupkey --batch --ref
func LookupKeyBatch(ctx context.Context, shasToBatchReader *io.PipeReader, lookupKeyBatchWriter *io.PipeWriter, wg *sync.WaitGroup, repoPath string) {
defer wg.Done()
defer shasToBatchReader.Close()
defer lookupKeyBatchWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
if err := git.NewCommand(ctx, "annex", "lookupkey", "--batch", "--ref").Run(&git.RunOpts{
Dir: repoPath,
Stdout: lookupKeyBatchWriter,
Stdin: shasToBatchReader,
Stderr: stderr,
}); err != nil {
_ = lookupKeyBatchWriter.CloseWithError(fmt.Errorf("git annex lookupkey --batch --ref [%s]: %w - %s", repoPath, err, errbuf.String()))
}
}
// CopyFromToBatch runs git -c annex.hardlink=true annex copy --batch-keys --from <remote> --to <remote>
func CopyFromToBatch(ctx context.Context, from, to string, keysToCopyReader *io.PipeReader, wg *sync.WaitGroup, repoPath string) {
defer wg.Done()
defer keysToCopyReader.Close()
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
var errbuf strings.Builder
if err := git.NewCommand(ctx, "-c", "annex.hardlink=true", "annex", "copy", "--batch-keys", "--from").AddDynamicArguments(from).AddArguments("--to").AddDynamicArguments(to).Run(&git.RunOpts{
Dir: repoPath,
Stdout: stdout,
Stdin: keysToCopyReader,
Stderr: stderr,
}); err != nil {
_ = keysToCopyReader.CloseWithError(fmt.Errorf("git annex copy --batch-keys --from <remote> --to <remote> [%s]: %w - %s", repoPath, err, errbuf.String()))
}
}
func ContentLocationFromKey(repoPath, key string) (string, error) {
contentLocation, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "contentlocation").AddDynamicArguments(key).RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil {
return "", fmt.Errorf("in %s: %s does not seem to be a valid annexed file: %w", repoPath, key, err)
}
contentLocation = strings.TrimSpace(contentLocation)
contentLocation = path.Clean("/" + contentLocation)[1:] // prevent directory traversals
contentLocation = path.Join(repoPath, contentLocation)
return contentLocation, nil
}
// return the absolute path of the content pointed to by the annex pointer stored in the git object
// errors if the content is not found in this repo
func ContentLocation(blob *git.Blob) (string, error) {
key, err := LookupKey(blob)
if err != nil {
return "", err
}
return ContentLocationFromKey(blob.Repo().Path, key)
}
// returns a stream open to the annex content
func Content(blob *git.Blob) (*os.File, error) {
contentLocation, err := ContentLocation(blob)
if err != nil {
return nil, err
}
return os.Open(contentLocation)
}
// whether the object appears to be a valid annex pointer
// does *not* verify if the content is actually in this repo;
// for that, use ContentLocation()
func IsAnnexed(blob *git.Blob) (bool, error) {
if !setting.Annex.Enabled {
return false, nil
}
// LookupKey is written to only return well-formed keys
// so the test is just to see if it errors
_, err := LookupKey(blob)
if err != nil {
if errors.Is(err, ErrBlobIsNotAnnexed) {
return false, nil
}
return false, err
}
return true, nil
}
// PathIsAnnexRepo determines if repoPath is a git-annex enabled repository
func PathIsAnnexRepo(repoPath string) bool {
_, _, err := git.NewCommand(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repoPath})
return err == nil
}
// IsAnnexRepo determines if repo is a git-annex enabled repository
func IsAnnexRepo(repo *git.Repository) bool {
_, _, err := git.NewCommand(repo.Ctx, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repo.Path})
return err == nil
}
var (
uuid2repoPathCache = make(map[string]string)
repoPath2uuidCache = make(map[string]string)
)
func Init() error {
if !setting.Annex.Enabled {
return nil
}
if !setting.Annex.DisableP2PHTTP {
log.Info("Populating the git-annex UUID cache with existing repositories")
start := time.Now()
if err := updateUUID2RepoPathCache(); err != nil {
return err
}
log.Info("Populating the git-annex UUID cache took %v", time.Since(start))
}
return nil
}
func updateUUID2RepoPathCache() error {
configFiles, err := filepath.Glob(filepath.Join(setting.RepoRootPath, "*", "*", "config"))
if err != nil {
return err
}
for _, configFile := range configFiles {
repoPath := strings.TrimSuffix(configFile, "/config")
_, ok := repoPath2uuidCache[repoPath]
if ok {
continue
}
config, err := ini.Load(configFile)
if err != nil {
continue
}
repoUUID := config.Section("annex").Key("uuid").Value()
if repoUUID != "" {
uuid2repoPathCache[repoUUID] = repoPath
repoPath2uuidCache[repoPath] = repoUUID
}
}
return nil
}
func repoPathFromUUIDCache(uuid string) (string, error) {
if repoPath, ok := uuid2repoPathCache[uuid]; ok {
return repoPath, nil
}
// If the cache didn't contain an entry for the UUID then update the cache and try again
if err := updateUUID2RepoPathCache(); err != nil {
return "", err
}
if repoPath, ok := uuid2repoPathCache[uuid]; ok {
return repoPath, nil
}
return "", fmt.Errorf("no repository known for UUID '%s'", uuid)
}
func checkValidity(uuid, repoPath string) (bool, error) {
stdout, _, err := git.NewCommand(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repoPath})
if err != nil {
return false, err
}
repoUUID := strings.TrimSpace(stdout)
return uuid == repoUUID, nil
}
func removeCachedEntries(uuid, repoPath string) {
delete(uuid2repoPathCache, uuid)
delete(repoPath2uuidCache, repoPath)
}
func UUID2RepoPath(uuid string) (string, error) {
// Get the current cache entry for the UUID
repoPath, err := repoPathFromUUIDCache(uuid)
if err != nil {
return "", err
}
// Check if it is still up-to-date
valid, err := checkValidity(uuid, repoPath)
if err != nil {
return "", err
}
if !valid {
// If it isn't, remove the cache entry and try again
removeCachedEntries(uuid, repoPath)
return UUID2RepoPath(uuid)
}
// Otherwise just return the cached entry
return repoPath, nil
}
// GuessContentType guesses the content type of the annexed blob.
func GuessContentType(blob *git.Blob) (typesniffer.SniffedType, error) {
r, err := Content(blob)
if err != nil {
return typesniffer.SniffedType{}, err
}
defer r.Close()
return typesniffer.DetectContentTypeFromReader(r)
}

View file

@ -16,6 +16,7 @@ import (
"strings"
"unicode/utf8"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@ -101,6 +102,12 @@ func Int64sToStrings(ints []int64) []string {
// EntryIcon returns the octicon class for displaying files/directories
func EntryIcon(entry *git.TreeEntry) string {
isAnnexed, _ := annex.IsAnnexed(entry.Blob())
if isAnnexed {
// Show git-annex files as binary files to differentiate them from non-annexed files
// TODO: find a more suitable icon, maybe something related to git-annex
return "file-binary"
}
switch {
case entry.IsLink():
te, _, err := entry.FollowLink()

View file

@ -126,6 +126,10 @@ func (b *blobReader) Close() error {
return nil
}
func (b *Blob) Repo() *Repository {
return b.repo
}
// Name returns name of the tree entry this blob object was created from (or empty string)
func (b *Blob) Name() string {
return b.name

View file

@ -457,12 +457,13 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
}
// AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests
// It also re-enables git-credential(1), which is used to test git-annex's HTTP support
func AllowLFSFiltersArgs() TrustedCmdArgs {
// Now here we should explicitly allow lfs filters to run
filteredLFSGlobalArgs := make(TrustedCmdArgs, len(globalCommandArgs))
j := 0
for _, arg := range globalCommandArgs {
if strings.Contains(string(arg), "lfs") {
if strings.Contains(string(arg), "lfs") || strings.Contains(string(arg), "credential") {
j--
} else {
filteredLFSGlobalArgs[j] = arg

View file

@ -106,3 +106,36 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s
}
}
}
// BlobsLessThanOrEqual32KiBFromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <=32KiB in size
func BlobsLessThanOrEqual32KiBFromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) {
defer wg.Done()
defer catFileCheckReader.Close()
scanner := bufio.NewScanner(catFileCheckReader)
defer func() {
_ = shasToBatchWriter.CloseWithError(scanner.Err())
}()
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 {
continue
}
fields := strings.Split(line, " ")
if len(fields) < 3 || fields[1] != "blob" {
continue
}
size, _ := strconv.Atoi(fields[2])
if size > 32*1024 {
continue
}
toWrite := []byte(fields[0] + "\n")
for len(toWrite) > 0 {
n, err := shasToBatchWriter.Write(toWrite)
if err != nil {
_ = catFileCheckReader.CloseWithError(err)
break
}
toWrite = toWrite[n:]
}
}
}

View file

@ -12,6 +12,7 @@ import (
"runtime"
"strings"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@ -86,8 +87,22 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
commands = strings.Fields(command)
args = commands[1:]
)
if p.IsInputFile {
isAnnexed, _ := annex.IsAnnexed(ctx.Blob)
// if a renderer wants to read a file, and we have annexed content, we can
// provide the annex key file location directly to the renderer. git-annex
// takes care of having that location be read-only, so no critical
// protection layer is needed. Moreover, the file readily exists, and
// expensive temporary files can be avoided, also allowing an operator
// to raise MAX_DISPLAY_FILE_SIZE without much negative impact.
if p.IsInputFile && isAnnexed {
// look for annexed content, will be empty, if there is none
annexContentLocation, _ := annex.ContentLocation(ctx.Blob)
// we call the renderer, even if there is no annex content present.
// showing the pointer file content is not much use, and a topical
// renderer might be able to produce something useful from the
// filename alone (present in ENV)
args = append(args, annexContentLocation)
} else if p.IsInputFile {
// write to temp file
f, err := os.CreateTemp("", "gitea_input")
if err != nil {
@ -130,6 +145,12 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
os.Environ(),
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
// also communicate the relative path of the to-be-rendered item.
// this enables the renderer to make use of the original file name
// and path, e.g., to make rendering or dtype-detection decisions
// that go beyond the originally matched extension. Even if the
// content is directly streamed to STDIN
"GITEA_RELATIVE_PATH="+ctx.RelativePath,
)
if !p.IsInputFile {
cmd.Stdin = input

View file

@ -67,14 +67,18 @@ type Header struct {
// RenderContext represents a render context
type RenderContext struct {
Ctx context.Context
RelativePath string // relative path from tree root of the branch
Type string
IsWiki bool
Links Links
Metas map[string]string
DefaultLink string
GitRepo *git.Repository
Ctx context.Context
RelativePath string // relative path from tree root of the branch
Type string
IsWiki bool
Links Links
Metas map[string]string
DefaultLink string
GitRepo *git.Repository
// reporting the target blob that is to-be-rendered enables
// deeper inspection in the handler for external renderer
// (i.e., more targeted handling of annexed files)
Blob *git.Blob
ShaExistCache map[string]bool
cancelFn func()
SidebarTocNode ast.Node

View file

@ -40,6 +40,7 @@ type ServCommandResults struct {
UserName string
UserEmail string
UserID int64
UserMode perm.AccessMode
OwnerName string
RepoName string
RepoID int64

25
modules/setting/annex.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"code.gitea.io/gitea/modules/log"
)
// Annex represents the configuration for git-annex
var Annex = struct {
Enabled bool `ini:"ENABLED"`
DisableP2PHTTP bool `ini:"DISABLE_P2PHTTP"`
}{}
func loadAnnexFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("annex")
if err := sec.MapTo(&Annex); err != nil {
log.Fatal("Failed to map Annex settings: %v", err)
}
if !sec.HasKey("DISABLE_P2PHTTP") {
// If DisableP2PHTTP is not explicitly set then use DisableHTTPGit as its default
Annex.DisableP2PHTTP = Repository.DisableHTTPGit
}
}

View file

@ -153,6 +153,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadCamoFrom(cfg)
loadI18nFrom(cfg)
loadGitFrom(cfg)
loadAnnexFrom(cfg)
loadMirrorFrom(cfg)
loadMarkupFrom(cfg)
loadQuotaFrom(cfg)

View file

@ -4,7 +4,9 @@
package util
import (
"io/fs"
"os"
"path/filepath"
"runtime"
"syscall"
"time"
@ -41,10 +43,48 @@ func Remove(name string) error {
return err
}
// RemoveAll removes the named file or (empty) directory with at most 5 attempts.
// MakeWritable recursively makes the named directory writable.
func MakeWritable(name string) error {
return filepath.WalkDir(name, func(path string, d fs.DirEntry, err error) error {
// NB: this is called WalkDir but it works on a single file too
if err == nil {
info, err := d.Info()
if err != nil {
return err
}
// Don't try chmod'ing symlinks (will fail with broken symlinks)
if info.Mode()&os.ModeSymlink != os.ModeSymlink {
// 0200 == u+w, in octal unix permission notation
err = os.Chmod(path, info.Mode()|0o200)
if err != nil {
return err
}
}
}
return nil
})
}
// RemoveAll removes the named file or directory with at most 5 attempts.
func RemoveAll(name string) error {
var err error
for i := 0; i < 5; i++ {
// Do chmod -R +w to help ensure the removal succeeds.
// In particular, in the git-annex case, this handles
// https://git-annex.branchable.com/internals/lockdown/ :
//
// > (The only bad consequence of this is that rm -rf .git
// > doesn't work unless you first run chmod -R +w .git)
err = MakeWritable(name)
if err != nil {
// try again
<-time.After(100 * time.Millisecond)
continue
}
err = os.RemoveAll(name)
if err == nil {
break

View file

@ -1317,6 +1317,7 @@ view_git_blame=Zobrazit git blame
video_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „video“.
audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „audio“.
stored_lfs=Uloženo pomocí Git LFS
stored_annex=Uloženo pomocí Git Annex
symbolic_link=Symbolický odkaz
executable_file=Spustitelný soubor
vendored = Vendorováno
@ -1342,6 +1343,7 @@ editor.upload_file=Nahrát soubor
editor.edit_file=Upravit soubor
editor.preview_changes=Náhled změn
editor.cannot_edit_lfs_files=LFS soubory nemohou být upravovány přes webové rozhraní.
editor.cannot_edit_annex_files=Annex soubory nemohou být upravovány přes webové rozhraní.
editor.cannot_edit_non_text_files=Binární soubory nemohou být upravovány přes webové rozhraní.
editor.edit_this_file=Upravit soubor
editor.this_file_locked=Soubor je uzamčen

View file

@ -1317,6 +1317,8 @@ view_git_blame=„git blame“ ansehen
video_not_supported_in_browser=Dein Browser unterstützt das HTML5-„video“-Tag nicht.
audio_not_supported_in_browser=Dein Browser unterstützt das HTML5-„audio“-Tag nicht.
stored_lfs=Gespeichert mit Git LFS
stored_annex=Gespeichert mit Git Annex
stored_annex_not_present = hier nicht vorhanden, versuche git annex whereis
symbolic_link=Softlink
executable_file=Ausführbare Datei
commit_graph=Commit-Graph
@ -1340,6 +1342,7 @@ editor.upload_file=Datei hochladen
editor.edit_file=Datei bearbeiten
editor.preview_changes=Vorschau der Änderungen
editor.cannot_edit_lfs_files=LFS-Dateien können im Webinterface nicht bearbeitet werden.
editor.cannot_edit_annex_files=Annex-Dateien können im Webinterface nicht bearbeitet werden.
editor.cannot_edit_non_text_files=Binärdateien können nicht im Webinterface bearbeitet werden.
editor.edit_this_file=Datei bearbeiten
editor.this_file_locked=Datei ist gesperrt

View file

@ -1314,6 +1314,7 @@ view_git_blame=Προβολή git blame
video_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «video».
audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «audio».
stored_lfs=Αποθηκεύτηκε με το Git LFS
stored_annex=Αποθηκεύτηκε με το Git Annex
symbolic_link=Symbolic link
executable_file=Εκτελέσιμο αρχείο
commit_graph=Γράφημα υποβολών
@ -1337,6 +1338,7 @@ editor.upload_file=Ανέβασμα αρχείου
editor.edit_file=Επεξεργασία αρχείου
editor.preview_changes=Προεπισκόπηση αλλαγών
editor.cannot_edit_lfs_files=Τα αρχεία LFS δεν μπορούν να επεξεργαστούν στη διεπαφή web.
editor.cannot_edit_annex_files=Τα αρχεία Annex δεν μπορούν να επεξεργαστούν στη διεπαφή web.
editor.cannot_edit_non_text_files=Τα δυαδικά αρχεία δεν μπορούν να επεξεργαστούν στη διεπαφή web.
editor.edit_this_file=Επεξεργασία αρχείου
editor.this_file_locked=Το αρχείο είναι κλειδωμένο

View file

@ -1337,6 +1337,8 @@ view_git_blame = View git blame
video_not_supported_in_browser = Your browser does not support the HTML5 "video" tag.
audio_not_supported_in_browser = Your browser does not support the HTML5 "audio" tag.
stored_lfs = Stored with Git LFS
stored_annex = Stored with Git Annex
stored_annex_not_present = not present here, try using git annex whereis
symbolic_link = Symbolic link
executable_file = Executable file
vendored = Vendored
@ -1364,6 +1366,7 @@ editor.upload_file = Upload file
editor.edit_file = Edit file
editor.preview_changes = Preview changes
editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface.
editor.cannot_edit_annex_files = Annex files cannot be edited in the web interface.
editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface.
editor.edit_this_file = Edit file
editor.this_file_locked = File is locked

View file

@ -1312,6 +1312,7 @@ view_git_blame=Ver Git blame
video_not_supported_in_browser=Su navegador no soporta el tag "video" de HTML5.
audio_not_supported_in_browser=Su navegador no soporta el tag "audio" de HTML5.
stored_lfs=Almacenados con Git LFS
stored_annex=Almacenados con Git Annex
symbolic_link=Enlace simbólico
executable_file=Archivo ejecutable
commit_graph=Gráfico de commits
@ -1335,6 +1336,7 @@ editor.upload_file=Subir archivo
editor.edit_file=Editar archivo
editor.preview_changes=Vista previa de los cambios
editor.cannot_edit_lfs_files=Los archivos LFS no se pueden editar en la interfaz web.
editor.cannot_edit_annex_files=Los archivos Annex no se pueden editar en la interfaz web.
editor.cannot_edit_non_text_files=Los archivos binarios no se pueden editar en la interfaz web.
editor.edit_this_file=Editar archivo
editor.this_file_locked=El archivo está bloqueado

View file

@ -951,6 +951,7 @@ file_copy_permalink=پرمالینک را کپی کنید
video_not_supported_in_browser=مرورگر شما از تگ video که در HTML5 تعریف شده است، پشتیبانی نمی کند.
audio_not_supported_in_browser=مرورگر شما از تگ audio که در HTML5 تعریف شده است، پشتیبانی نمی کند.
stored_lfs=ذخیره شده با GIT LFS
stored_annex=ذخیره شده با GIT Annex
symbolic_link=پیوند نمادین
commit_graph=نمودار کامیت
commit_graph.select=انتخاب برنچها
@ -968,6 +969,7 @@ editor.upload_file=بارگذاری پرونده
editor.edit_file=ویرایش پرونده
editor.preview_changes=پیش نمایش تغییرات
editor.cannot_edit_lfs_files=پرونده های LFS در صحفه وب قابل تغییر نیست.
editor.cannot_edit_annex_files=پرونده های Annex در صحفه وب قابل تغییر نیست.
editor.cannot_edit_non_text_files=پرونده‎های دودویی در صفحه وب قابل تغییر نیست.
editor.edit_this_file=ویرایش پرونده
editor.this_file_locked=پرونده قفل شده است

View file

@ -1318,6 +1318,7 @@ view_git_blame=Voir Git blame
video_not_supported_in_browser=Votre navigateur ne supporte pas la balise « vidéo » HTML5.
audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « audio » HTML5.
stored_lfs=Stocké avec Git LFS
stored_annex=Stocké avec Git Annex
symbolic_link=Lien symbolique
executable_file=Fichier exécutable
vendored = Vendored
@ -1343,6 +1344,7 @@ editor.upload_file=Téléverser un fichier
editor.edit_file=Modifier le fichier
editor.preview_changes=Aperçu des modifications
editor.cannot_edit_lfs_files=Les fichiers LFS ne peuvent pas être modifiés dans l'interface web.
editor.cannot_edit_annex_files=Les fichiers Annex ne peuvent pas être modifiés dans l'interface web.
editor.cannot_edit_non_text_files=Les fichiers binaires ne peuvent pas être édités dans l'interface web.
editor.edit_this_file=Modifier le fichier
editor.this_file_locked=Le fichier est verrouillé

View file

@ -787,6 +787,7 @@ file_too_large=Ez a fájl túl nagy ahhoz, hogy megjelenítsük.
video_not_supported_in_browser=A böngésző nem támogatja a HTML5 video tag-et.
audio_not_supported_in_browser=A böngésző nem támogatja a HTML5 audio tag-et.
stored_lfs=Git LFS-el eltárolva
stored_annex=Git Annex-el eltárolva
symbolic_link=Szimbolikus hivatkozás
commit_graph=Commit gráf
commit_graph.hide_pr_refs=Pull request-ek elrejtése
@ -799,6 +800,7 @@ editor.upload_file=Fájl feltöltése
editor.edit_file=Fájl szerkesztése
editor.preview_changes=Változások előnézete
editor.cannot_edit_lfs_files=LFS fájlok nem szerkeszthetőek a webes felületen.
editor.cannot_edit_annex_files=Annex fájlok nem szerkeszthetőek a webes felületen.
editor.cannot_edit_non_text_files=Bináris fájlok nem szerkeszthetőek a webes felületen.
editor.edit_this_file=Fájl szerkesztése
editor.this_file_locked=Zárolt állomány

View file

@ -596,6 +596,7 @@ file_permalink=Permalink
file_too_large=Berkas terlalu besar untuk ditampilkan.
stored_lfs=Tersimpan dengan GIT LFS
stored_annex=Tersimpan dengan GIT Annex
commit_graph=Grafik Komit
blame=Salahkan
normal_view=Pandangan Normal
@ -607,6 +608,7 @@ editor.upload_file=Unggah Berkas
editor.edit_file=Sunting Berkas
editor.preview_changes=Tinjau Perubahan
editor.cannot_edit_lfs_files=Berkas LFS tidak dapat disunting dalam antarmuka web.
editor.cannot_edit_annex_files=Berkas Annex tidak dapat disunting dalam antarmuka web.
editor.cannot_edit_non_text_files=Berkas biner tidak dapat disunting dalam antarmuka web.
editor.edit_this_file=Sunting Berkas
editor.this_file_locked=Berkas terkunci

View file

@ -680,6 +680,7 @@ file_view_rendered=Skoða Unnið
file_copy_permalink=Afrita Varanlega Slóð
stored_lfs=Geymt með Git LFS
stored_annex=Geymt með Git Annex
commit_graph.hide_pr_refs=Fela Sameiningarbeiðnir
commit_graph.monochrome=Einlitað
commit_graph.color=Litað

View file

@ -1267,6 +1267,7 @@ view_git_blame=Visualizza git incolpa
video_not_supported_in_browser=Il tuo browser non supporta le etichette "video" di HTML5.
audio_not_supported_in_browser=Il tuo browser non supporta le etichette "audio" di HTML5.
stored_lfs=Memorizzati con Git LFS
stored_annex=Memorizzati con Git Annex
symbolic_link=Link Simbolico
commit_graph=Grafico dei commit
commit_graph.select=Seleziona rami
@ -1285,6 +1286,7 @@ editor.upload_file=Carica file
editor.edit_file=Modifica file
editor.preview_changes=Anteprima modifiche
editor.cannot_edit_lfs_files=I file LFS non possono essere modificati nell'interfaccia web.
editor.cannot_edit_annex_files=I file Annex non possono essere modificati nell'interfaccia web.
editor.cannot_edit_non_text_files=I file binari non possono essere modificati tramite interfaccia web.
editor.edit_this_file=Modifica file
editor.this_file_locked=Il file è bloccato

View file

@ -1305,6 +1305,7 @@ view_git_blame=Git Blameを表示
video_not_supported_in_browser=このブラウザはHTML5のvideoタグをサポートしていません。
audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサポートしていません。
stored_lfs=Git LFSで保管されています
stored_annex=Git Annexで保管されています
symbolic_link=シンボリック リンク
executable_file=実行ファイル
commit_graph=コミットグラフ
@ -1328,6 +1329,7 @@ editor.upload_file=ファイルをアップロード
editor.edit_file=ファイルを編集
editor.preview_changes=変更をプレビュー
editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。
editor.cannot_edit_annex_files=AnnexのファイルはWebインターフェースで編集できません。
editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。
editor.edit_this_file=ファイルを編集
editor.this_file_locked=ファイルはロックされています

View file

@ -821,6 +821,7 @@ file_too_large=보여주기에는 파일이 너무 큽니다.
video_not_supported_in_browser=당신의 브라우저가 HTML5의 "video" 태그를 지원하지 않습니다.
audio_not_supported_in_browser=당신의 브라우저가 HTML5의 "audio" 태그를 지원하지 않습니다.
stored_lfs=Git LFS에 저장되어 있습니다
stored_annex=Git Annex에 저장되어 있습니다
commit_graph=커밋 그래프
editor.new_file=새 파일

View file

@ -1316,6 +1316,7 @@ view_git_blame=Apskatīt Git izmaiņu veicējus
video_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "video".
audio_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "audio".
stored_lfs=Saglabāts Git LFS
stored_annex=Saglabāts Git Annex
symbolic_link=Simboliska saite
executable_file=Izpildāma datne
commit_graph=Iesūtījumu karte
@ -1339,6 +1340,7 @@ editor.upload_file=Augšupielādēt datni
editor.edit_file=Labot datni
editor.preview_changes=Priekšskatīt izmaiņas
editor.cannot_edit_lfs_files=LFS datnes tīmekļa saskarnē nevar labot.
editor.cannot_edit_annex_files=Annex datnes tīmekļa saskarnē nevar labot.
editor.cannot_edit_non_text_files=Binārās datnes tīmekļa saskarnē nevar labot.
editor.edit_this_file=Labot datni
editor.this_file_locked=Datne ir slēgta
@ -4027,4 +4029,4 @@ filepreview.lines = %[1]d. līdz %[2]d. rinda %[3]s
filepreview.truncated = Priekšskatījums tika saīsināts
[translation_meta]
test = Šī ir pārbaudes virkne. Tā netiek attēlota Forgejo saskarnē, bet tiek izmantota pārbaudes nolūkiem. Droši var ievadīt "ok", lai ietaupītu laiku (vai kādu jautru faktu pēc izvēles), lai sasniegtu to saldo 100% pabeigšanas atzīmi.
test = Šī ir pārbaudes virkne. Tā netiek attēlota Forgejo saskarnē, bet tiek izmantota pārbaudes nolūkiem. Droši var ievadīt "ok", lai ietaupītu laiku (vai kādu jautru faktu pēc izvēles), lai sasniegtu to saldo 100% pabeigšanas atzīmi.

View file

@ -1285,6 +1285,7 @@ view_git_blame=Bekijk git blame
video_not_supported_in_browser=Uw browser ondersteunt de HTML5 "video" element niet.
audio_not_supported_in_browser=Uw browser ondersteunt de HTML5 "audio" element niet.
stored_lfs=Opgeslagen met Git LFS
stored_annex=Opgeslagen met Git Annex
symbolic_link=Symbolische link
commit_graph=Commit grafiek
commit_graph.select=Selecteer branches
@ -1303,6 +1304,7 @@ editor.upload_file=Upload bestand
editor.edit_file=Bewerk bestand
editor.preview_changes=Voorbeeld tonen
editor.cannot_edit_lfs_files=LFS-bestanden kunnen niet worden bewerkt in de webinterface.
editor.cannot_edit_annex_files=Annex-bestanden kunnen niet worden bewerkt in de webinterface.
editor.cannot_edit_non_text_files=Binaire bestanden kunnen niet worden bewerkt in de webinterface.
editor.edit_this_file=Bewerk bestand
editor.this_file_locked=Bestand is vergrendeld

View file

@ -1249,6 +1249,7 @@ file_copy_permalink=Kopiuj bezpośredni odnośnik
video_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "video".
audio_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "audio".
stored_lfs=Przechowane za pomocą Git LFS
stored_annex=Przechowane za pomocą Git Annex
symbolic_link=Dowiązanie symboliczne
commit_graph=Wykres commitów
commit_graph.select=Wybierz gałęzie
@ -1266,6 +1267,7 @@ editor.upload_file=Wyślij plik
editor.edit_file=Edytuj plik
editor.preview_changes=Podgląd zmian
editor.cannot_edit_lfs_files=Pliki LFS nie mogą być edytowane poprzez interfejs przeglądarkowy.
editor.cannot_edit_annex_files=Pliki Annex nie mogą być edytowane poprzez interfejs przeglądarkowy.
editor.cannot_edit_non_text_files=Pliki binarne nie mogą być edytowane poprzez interfejs przeglądarkowy.
editor.edit_this_file=Edytuj plik
editor.this_file_locked=Plik jest zablokowany

View file

@ -1310,6 +1310,7 @@ view_git_blame=Ver git blame
video_not_supported_in_browser=Seu navegador não tem suporte para a tag "video" do HTML5.
audio_not_supported_in_browser=Seu navegador não tem suporte para a tag "audio" do HTML5.
stored_lfs=Armazenado com Git LFS
stored_annex=Armazenado com Git Annex
symbolic_link=Link simbólico
executable_file=Arquivo executável
commit_graph=Gráfico de commits
@ -1333,6 +1334,7 @@ editor.upload_file=Enviar arquivo
editor.edit_file=Editar arquivo
editor.preview_changes=Pré-visualizar alterações
editor.cannot_edit_lfs_files=Arquivos LFS não podem ser editados na interface web.
editor.cannot_edit_annex_files=Arquivos Annex não podem ser editados na interface web.
editor.cannot_edit_non_text_files=Arquivos binários não podem ser editados na interface web.
editor.edit_this_file=Editar arquivo
editor.this_file_locked=Arquivo está bloqueado

View file

@ -1321,6 +1321,7 @@ view_git_blame=Ver git blame
video_not_supported_in_browser=O seu navegador não suporta a etiqueta "video" do HTML5.
audio_not_supported_in_browser=O seu navegador não suporta a etiqueta "audio" do HTML5.
stored_lfs=Armazenado com Git LFS
stored_annex=Armazenado com Git Annex
symbolic_link=Ligação simbólica
executable_file=Ficheiro executável
vendored=Externo
@ -1346,6 +1347,7 @@ editor.upload_file=Carregar ficheiro
editor.edit_file=Editar ficheiro
editor.preview_changes=Pré-visualizar modificações
editor.cannot_edit_lfs_files=Ficheiros LFS não podem ser editados na interface web.
editor.cannot_edit_annex_files=Ficheiros Annex não podem ser editados na interface web.
editor.cannot_edit_non_text_files=Ficheiros binários não podem ser editados na interface da web.
editor.edit_this_file=Editar ficheiro
editor.this_file_locked=Ficheiro bloqueado

View file

@ -1304,6 +1304,7 @@ view_git_blame=Показать git blame
video_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «video».
audio_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «audio».
stored_lfs=Хранится Git LFS
stored_annex=Хранится Git Annex
symbolic_link=Символическая ссылка
executable_file=Исполняемый файл
commit_graph=Граф коммитов
@ -1327,6 +1328,7 @@ editor.upload_file=Загрузить файл
editor.edit_file=Редактировать файл
editor.preview_changes=Просмотр изменений
editor.cannot_edit_lfs_files=LFS файлы невозможно редактировать в веб-интерфейсе.
editor.cannot_edit_annex_files=Annex файлы невозможно редактировать в веб-интерфейсе.
editor.cannot_edit_non_text_files=Двоичные файлы нельзя редактировать в веб-интерфейсе.
editor.edit_this_file=Редактировать файл
editor.this_file_locked=Файл заблокирован

View file

@ -889,6 +889,7 @@ file_copy_permalink=පිටපත් මාමලින්ක්
video_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'වීඩියෝ' ටැගය සඳහා සහය නොදක්වයි.
audio_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'ශ්රව්ය' ටැගය සඳහා සහය නොදක්වයි.
stored_lfs=Git LFS සමඟ ගබඩා
stored_annex=Git Annex සමඟ ගබඩා
symbolic_link=සංකේතාත්මක සබැඳිය
commit_graph=ප්රස්තාරය කැප
commit_graph.select=ශාඛා තෝරන්න
@ -906,6 +907,7 @@ editor.upload_file=ගොනුව උඩුගත කරන්න
editor.edit_file=ගොනුව සංස්කරණය
editor.preview_changes=වෙනස්කම් පෙරදසුන
editor.cannot_edit_lfs_files=LFS ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
editor.cannot_edit_annex_files=Annex ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
editor.cannot_edit_non_text_files=ද්විමය ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
editor.edit_this_file=ගොනුව සංස්කරණය
editor.this_file_locked=ගොනුවට අගුළු ලා ඇත

View file

@ -1010,6 +1010,7 @@ view_git_blame=Zobraziť Git Blame
video_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'video'.
audio_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'audio'.
stored_lfs=Uložené pomocou Git LFS
stored_annex=Uložené pomocou Git Annex
symbolic_link=Symbolický odkaz
commit_graph=Graf commitov
line=riadok

View file

@ -910,6 +910,7 @@ file_too_large=Filen är för stor för att visas.
video_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen "video".
audio_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen "audio".
stored_lfs=Sparad med Git LFS
stored_annex=Sparad med Git Annex
symbolic_link=Symbolisk länk
commit_graph=Commitgraf
commit_graph.monochrome=Mono
@ -923,6 +924,7 @@ editor.upload_file=Ladda upp fil
editor.edit_file=Redigera fil
editor.preview_changes=Förhandsgranska ändringar
editor.cannot_edit_lfs_files=LFS-filer kan inte redigeras i webbgränssnittet.
editor.cannot_edit_annex_files=Annex-filer kan inte redigeras i webbgränssnittet.
editor.cannot_edit_non_text_files=Binära filer kan inte redigeras genom webbgränssnittet.
editor.edit_this_file=Redigera fil
editor.this_file_locked=Filen är låst

View file

@ -1290,6 +1290,7 @@ view_git_blame=Git Suç Görüntüle
video_not_supported_in_browser=Tarayıcınız HTML5 'video' etiketini desteklemiyor.
audio_not_supported_in_browser=Tarayıcınız HTML5 'audio' etiketini desteklemiyor.
stored_lfs=Git LFS ile depolandı
stored_annex=Git Annex ile depolandı
symbolic_link=Sembolik Bağlantı
executable_file=Çalıştırılabilir Dosya
commit_graph=İşleme Grafiği
@ -1313,6 +1314,7 @@ editor.upload_file=Dosya Yükle
editor.edit_file=Dosyayı Düzenle
editor.preview_changes=Değişiklikleri Önizle
editor.cannot_edit_lfs_files=LFS dosyaları web arayüzünde düzenlenemez.
editor.cannot_edit_annex_files=Annex dosyaları web arayüzünde düzenlenemez.
editor.cannot_edit_non_text_files=Bu tür dosyalar web arayüzünden düzenlenemez.
editor.edit_this_file=Dosyayı Düzenle
editor.this_file_locked=Dosya kilitlendi

View file

@ -1243,6 +1243,7 @@ file_copy_permalink=Копіювати постійне посилання
video_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «video».
audio_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «audio».
stored_lfs=Збережено з Git LFS
stored_annex=Збережено з Git Annex
symbolic_link=Символічне посилання
commit_graph=Графік комітів
commit_graph.select=Виберіть гілки
@ -1260,6 +1261,7 @@ editor.upload_file=Завантажити файл
editor.edit_file=Редагувати файл
editor.preview_changes=Попередній перегляд змін
editor.cannot_edit_lfs_files=Файли LFS не можна редагувати в веб-інтерфейсі.
editor.cannot_edit_annex_files=Файли Annex не можна редагувати в веб-інтерфейсі.
editor.cannot_edit_non_text_files=Бінарні файли не можливо редагувати у веб-інтерфейсі.
editor.edit_this_file=Редагувати файл
editor.this_file_locked=Файл заблоковано

View file

@ -1319,6 +1319,7 @@ view_git_blame=查看 Git Blame
video_not_supported_in_browser=您的浏览器不支持 HTML5 “video” 标签。
audio_not_supported_in_browser=您的浏览器不支持 HTML5 “audio” 标签。
stored_lfs=存储到Git LFS
stored_annex=存储到Git Annex
symbolic_link=符号链接
executable_file=可执行文件
vendored = Vendored
@ -1344,6 +1345,7 @@ editor.upload_file=上传文件
editor.edit_file=编辑文件
editor.preview_changes=预览变更
editor.cannot_edit_lfs_files=无法在 web 界面中编辑 lfs 文件。
editor.cannot_edit_annex_files=无法在 web 界面中编辑 lfs 文件。
editor.cannot_edit_non_text_files=网页不能编辑二进制文件。
editor.edit_this_file=编辑文件
editor.this_file_locked=文件已锁定

View file

@ -472,6 +472,7 @@ file_view_raw=查看原始文件
file_permalink=永久連結
stored_lfs=儲存到到 Git LFS
stored_annex=儲存到到 Git Annex
editor.preview_changes=預覽更改
editor.or=
@ -1132,4 +1133,4 @@ runners.labels = 標籤
[projects]
[git.filemode]
[git.filemode]

View file

@ -1293,6 +1293,7 @@ view_git_blame=檢視 Git Blame
video_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「video」標籤。
audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「audio」標籤。
stored_lfs=已使用 Git LFS 儲存
stored_annex=已使用 Git Annex 儲存
symbolic_link=符號連結
commit_graph=提交線圖
commit_graph.select=選擇分支
@ -1312,6 +1313,7 @@ editor.upload_file=上傳檔案
editor.edit_file=編輯檔案
editor.preview_changes=預覽變更
editor.cannot_edit_lfs_files=無法在 web 介面中編輯 LFS 檔。
editor.cannot_edit_annex_files=無法在 web 介面中編輯 Annex 檔。
editor.cannot_edit_non_text_files=網站介面不能編輯二進位檔案。
editor.edit_this_file=編輯檔案
editor.this_file_locked=檔案已被鎖定

2
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "forgejo",
"name": "forgejo-aneksajo",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
authmodel "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/git"
@ -167,6 +168,8 @@ func InitWebInstalled(ctx context.Context) {
actions_service.Init()
mustInit(annex.Init)
// Finally start up the cron
cron.NewContext(ctx)
}

View file

@ -81,12 +81,14 @@ func ServCommand(ctx *context.PrivateContext) {
ownerName := ctx.Params(":owner")
repoName := ctx.Params(":repo")
mode := perm.AccessMode(ctx.FormInt("mode"))
verbs := ctx.FormStrings("verb")
// Set the basic parts of the results to return
results := private.ServCommandResults{
RepoName: repoName,
OwnerName: ownerName,
KeyID: keyID,
UserMode: perm.AccessModeNone,
}
// Now because we're not translating things properly let's just default some English strings here
@ -287,8 +289,10 @@ func ServCommand(ctx *context.PrivateContext) {
repo.IsPrivate ||
owner.Visibility.IsPrivate() ||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
(setting.Annex.Enabled && len(verbs) > 0 && verbs[0] == "git-annex-shell") || // git-annex has its own permission enforcement, for which we expose results.UserMode
setting.Service.RequireSignInView) {
if key.Type == asymkey_model.KeyTypeDeploy {
results.UserMode = deployKey.Mode
if deployKey.Mode < mode {
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
@ -310,9 +314,9 @@ func ServCommand(ctx *context.PrivateContext) {
return
}
userMode := perm.UnitAccessMode(unitType)
results.UserMode = perm.UnitAccessMode(unitType)
if userMode < mode {
if results.UserMode < mode {
log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr())
ctx.JSON(http.StatusUnauthorized, private.Response{
UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName),
@ -353,6 +357,7 @@ func ServCommand(ctx *context.PrivateContext) {
})
return
}
results.UserMode = perm.AccessModeWrite
results.RepoID = repo.ID
}
@ -381,13 +386,14 @@ func ServCommand(ctx *context.PrivateContext) {
return
}
}
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nUserMode: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
results.IsWiki,
results.DeployKeyID,
results.KeyID,
results.KeyName,
results.UserName,
results.UserID,
results.UserMode,
results.OwnerName,
results.RepoName,
results.RepoID)

146
routers/web/repo/annex.go Normal file
View file

@ -0,0 +1,146 @@
package repo
import (
"context"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"strings"
"syscall"
"time"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
services_context "code.gitea.io/gitea/services/context"
)
type p2phttpRecordType struct {
CancelFunc func()
LastUsed time.Time
Port string
}
var p2phttpRecords = make(map[string]*p2phttpRecordType)
// AnnexP2PHTTP implements git-annex smart HTTP support by delegating to git annex p2phttp
func AnnexP2PHTTP(ctx *services_context.Context) {
uuid := ctx.Params(":uuid")
repoPath, err := annex.UUID2RepoPath(uuid)
if err != nil {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return
}
parts := strings.Split(repoPath, "/")
repoName := strings.TrimSuffix(parts[len(parts)-1], ".git")
owner := parts[len(parts)-2]
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return
}
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if !(ctx.Req.Method == "GET" && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
ctx.Req.Method == "POST" && p.CanAccess(perm.AccessModeWrite, unit.TypeCode) ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/checkpresent") && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/keeplocked") ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/lockcontent")) {
// GET requests require at least read access; POST requests for
// anything but checkpresent, lockcontent, and keeplocked
// require write permissions; POST requests for checkpresent
// only require read permissions, as it really is just a read.
// POST requests for lockcontent and keeplocked require no
// authentication at all, as is also the case for the
// authentication in the git-annex-p2phttp server. See
// https://git-annex.branchable.com/bugs/p2phttp__58___drop_difference_wideopen_unauth-readonly/
// for reasoning.
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
p2phttpRecord, p2phttpProcessExists := p2phttpRecords[uuid]
if p2phttpProcessExists {
p2phttpRecord.LastUsed = time.Now()
} else {
// Start a new p2phttp process for the requested repository
// There is a race condition here with the port selection, ideally git annex p2phttp could just listen on a unix socket...
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Error("Failed to listen on a free port: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
hopefullyFreePort := strings.SplitN(lis.Addr().String(), ":", 2)[1]
lis.Close()
p2phttpCtx, p2phttpCtxCancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "annex", "p2phttp", "-J2", "--bind", "127.0.0.1", "--wideopen", "--port", hopefullyFreePort)
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGINT,
}
cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }
_ = cmd.Run()
}(p2phttpCtx)
graceful.GetManager().RunAtTerminate(p2phttpCtxCancel)
// Wait for the p2phttp server to get ready
start := time.Now()
sleepDuration := 1 * time.Millisecond
for {
if time.Since(start) > 5*time.Second {
p2phttpCtxCancel()
log.Error("Failed to start the p2phttp server in a reasonable amount of time")
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
conn, err := net.Dial("tcp", "127.0.0.1:"+hopefullyFreePort)
if err == nil {
conn.Close()
break
}
time.Sleep(sleepDuration)
sleepDuration *= 2
if sleepDuration > 1*time.Second {
sleepDuration = 1 * time.Second
}
}
p2phttpRecord = &p2phttpRecordType{CancelFunc: p2phttpCtxCancel, LastUsed: time.Now(), Port: hopefullyFreePort}
p2phttpRecords[uuid] = p2phttpRecord
}
// Cleanup p2phttp processes that haven't been used for a while
for uuid, record := range p2phttpRecords {
if time.Since(record.LastUsed) > 5*time.Minute {
record.CancelFunc()
delete(p2phttpRecords, uuid)
}
}
url, err := url.Parse("http://127.0.0.1:" + p2phttpRecord.Port + strings.TrimPrefix(ctx.Req.RequestURI, "/git-annex-p2phttp"))
if err != nil {
log.Error("Failed to parse URL: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
proxy := httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.Out.URL = url
},
}
proxy.ServeHTTP(ctx.Resp, ctx.Req)
}

View file

@ -23,6 +23,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv"
@ -72,7 +73,21 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner
return st
}
st, err := blob.GuessContentType()
isAnnexed, err := annex.IsAnnexed(blob)
if err != nil {
log.Error("IsAnnexed failed: %v", err)
return st
}
if isAnnexed {
st, err = annex.GuessContentType(blob)
if err != nil {
log.Error("GuessContentType failed: %v", err)
return st
}
return st
}
st, err = blob.GuessContentType()
if err != nil {
log.Error("GuessContentType failed: %v", err)
return st
@ -90,18 +105,18 @@ func SourceCommitURL(owner, name string, commit *git.Commit) string {
return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/src/commit/" + url.PathEscape(commit.ID.String())
}
// RawCommitURL creates a relative URL for the raw commit in the given repository
func RawCommitURL(owner, name string, commit *git.Commit) string {
return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/raw/commit/" + url.PathEscape(commit.ID.String())
// MediaCommitURL creates a relative URL for the commit media (plain git, LFS, or annex content) in the given repository
func MediaCommitURL(owner, name string, commit *git.Commit) string {
return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/media/commit/" + url.PathEscape(commit.ID.String())
}
// setPathsCompareContext sets context data for source and raw paths
func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head)
ctx.Data["RawPath"] = RawCommitURL(headOwner, headName, head)
ctx.Data["RawPath"] = MediaCommitURL(headOwner, headName, head)
if base != nil {
ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, base)
ctx.Data["BeforeRawPath"] = RawCommitURL(headOwner, headName, base)
ctx.Data["BeforeRawPath"] = MediaCommitURL(headOwner, headName, base)
}
}

View file

@ -9,6 +9,7 @@ import (
"time"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
@ -79,6 +80,26 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
}
closed = true
// check for git-annex files
// (this code is weirdly redundant because I'm trying not to delete any lines in order to make merges easier)
isAnnexed, err := annex.IsAnnexed(blob)
if err != nil {
ctx.ServerError("annex.IsAnnexed", err)
return err
}
if isAnnexed {
content, err := annex.Content(blob)
if err != nil {
// XXX are there any other possible failure cases here?
// there are, there could be unrelated io errors; those should be ctx.ServerError()s
ctx.NotFound("annex.Content", err)
return err
}
defer content.Close()
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, content)
return nil
}
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
}

View file

@ -10,6 +10,7 @@ import (
gocontext "context"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
@ -78,7 +79,24 @@ func httpBase(ctx *context.Context) *serviceHandler {
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true
} else {
isPull = ctx.Req.Method == "GET"
// In addition to GET requests, HEAD requests are also "pull"
// operations (reads), so they should also not require
// authentication. This is necessary for git-annex to operate
// properly, as it emits HEAD requests to check for the
// existence of keys, e.g. before dropping locally, and asking
// for authentication would break unauthenticated http usage in
// this situation.
// It should be safe to make all HEAD requests require no
// authentication, but as it is only necessary for the
// annex/objects endpoints to fix git-annex' drop operations it
// is limited to those for now.
r, err := regexp.Compile("^/?" + username + "/" + reponame + "(.git)?/annex/objects")
if err != nil {
ctx.ServerError("failed to create URL path regex", err)
return nil
}
isPull = ctx.Req.Method == "GET" ||
r.MatchString(ctx.Req.URL.Path) && ctx.Req.Method == "HEAD"
}
var accessMode perm.AccessMode
@ -545,6 +563,42 @@ func GetInfoRefs(ctx *context.Context) {
}
}
// GetConfig implements fetching the git config of a repository
func GetConfig(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
setHeaderNoCache(ctx)
config, err := os.ReadFile(filepath.Join(h.getRepoDir(), "config"))
if err != nil {
log.Error("Failed to read git config file: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
if !setting.Annex.DisableP2PHTTP {
appURL, err := url.Parse(setting.AppURL)
if err != nil {
log.Error("Could not parse 'setting.AppURL': %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
if appURL.Port() == "" {
// If there is no port set then set the http(s) default ports.
// Without this, git-annex would try its own default port (9417) and fail.
if appURL.Scheme == "http" {
appURL.Host += ":80"
}
if appURL.Scheme == "https" {
appURL.Host += ":443"
}
}
config = append(config, []byte("[annex]\n\turl = annex+"+appURL.String()+"git-annex-p2phttp\n")...)
}
ctx.Resp.Header().Set("Content-Type", "text/plain")
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", len(config)))
http.ServeContent(ctx.Resp, ctx.Req, "config", time.Now(), bytes.NewReader(config))
}
}
// GetTextFile implements Git dumb HTTP
func GetTextFile(p string) func(*context.Context) {
return func(ctx *context.Context) {
@ -597,3 +651,34 @@ func GetIdxFile(ctx *context.Context) {
h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
}
}
// GetAnnexObject implements git-annex dumb HTTP
func GetAnnexObject(ctx *context.Context) {
h := httpBase(ctx)
if h != nil {
// git-annex objects are stored in .git/annex/objects/{hash1}/{hash2}/{key}/{key}
// where key is a string containing the size and (usually SHA256) checksum of the file,
// and hash1+hash2 are the first few bits of the md5sum of key itself.
// ({hash1}/{hash2}/ is just there to avoid putting too many files in one directory)
// ref: https://git-annex.branchable.com/internals/hashing/
// keyDir should = key, but we don't enforce that
object := filepath.Join(ctx.Params("hash1"), ctx.Params("hash2"), ctx.Params("keyDir"), ctx.Params("key"))
// Sanitize the input against directory traversals.
//
// This works because at the filesystem root, "/.." = "/";
// So if a path starts rooted ("/"), path.Clean(), which
// path.Join() calls internally, removes all '..' prefixes.
// After, this unroots the path unconditionally ([1:]), which
// works because we know the input is never supposed to be rooted.
//
// The router code probably also disallows "..", so this
// should be redundant, but it's defensive to keep it
// whenever touching filesystem paths with user input.
object = filepath.Join(string(filepath.Separator), object)[1:]
setHeaderCacheForever(ctx)
h.sendFile(ctx, "application/octet-stream", "annex/objects/"+object)
}
}

View file

@ -34,6 +34,7 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
@ -209,14 +210,59 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
}
type fileInfo struct {
isTextFile bool
isLFSFile bool
fileSize int64
lfsMeta *lfs.Pointer
st typesniffer.SniffedType
isTextFile bool
isLFSFile bool
isAnnexFile bool
isAnnexFilePresent bool
fileSize int64
lfsMeta *lfs.Pointer
st typesniffer.SniffedType
}
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
isAnnexed, err := annex.IsAnnexed(blob)
if err != nil {
return nil, nil, nil, err
}
if isAnnexed {
// TODO: this code could be merged with the LFS case, especially the redundant type sniffer,
// but it is *currently* written this way to make merging with the non-annex upstream easier:
// this way, the git-annex patch is (mostly) pure additions.
annexContent, err := annex.Content(blob)
if err != nil {
// If annex.Content returns an error it can mean that the blob does not
// refer to an annexed file or that it is not present here. Since we already
// checked that it is annexed the latter must be the case. So we return the
// content of the blob instead and indicate that the file is indeed annexed,
// but not present here. The template can then communicate the situation.
dataRc, err := blob.DataAsync()
if err != nil {
return nil, nil, nil, err
}
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
return buf, dataRc, &fileInfo{st.IsText(), false, true, false, blob.Size(), nil, st}, nil
}
stat, err := annexContent.Stat()
if err != nil {
return nil, nil, nil, err
}
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(annexContent, buf)
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
return buf, annexContent, &fileInfo{st.IsText(), false, true, true, stat.Size(), nil, st}, nil
}
dataRc, err := blob.DataAsync()
if err != nil {
return nil, nil, nil, err
@ -231,18 +277,18 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
// FIXME: what happens when README file is an image?
if !isTextFile || !setting.LFS.StartServer {
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
return buf, dataRc, &fileInfo{isTextFile, false, false, false, blob.Size(), nil, st}, nil
}
pointer, _ := lfs.ReadPointerFromBuffer(buf)
if !pointer.IsValid() { // fallback to plain file
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
return buf, dataRc, &fileInfo{isTextFile, false, false, false, blob.Size(), nil, st}, nil
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
if err != nil { // fallback to plain file
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
return buf, dataRc, &fileInfo{isTextFile, false, false, false, blob.Size(), nil, st}, nil
}
dataRc.Close()
@ -262,7 +308,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
st = typesniffer.DetectContentType(buf)
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
return buf, dataRc, &fileInfo{st.IsText(), true, false, false, meta.Size, &meta.Pointer, st}, nil
}
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
@ -325,6 +371,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Blob: target.Blob(),
}, rd)
if err != nil {
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
@ -447,10 +494,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
isDisplayingSource := ctx.FormString("display") == "source"
isDisplayingRendered := !isDisplayingSource
if fInfo.isLFSFile {
if fInfo.isLFSFile || fInfo.isAnnexFile {
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
if fInfo.isAnnexFile {
// pre-git-annex v7, all annexed files were represented in-repo as symlinks;
// but we pretend they aren't, since that's a distracting quirk of git-annex
// and not a meaningful choice on the user's part
ctx.Data["FileIsSymlink"] = false
}
isRepresentableAsText := fInfo.st.IsRepresentableAsText()
if !isRepresentableAsText {
// If we can't show plain text, always try to render.
@ -458,6 +512,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
isDisplayingRendered = true
}
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
ctx.Data["IsAnnexFile"] = fInfo.isAnnexFile
ctx.Data["IsAnnexFilePresent"] = fInfo.isAnnexFilePresent
ctx.Data["FileSize"] = fInfo.fileSize
ctx.Data["IsTextFile"] = fInfo.isTextFile
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
@ -492,6 +548,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
// Assume file is not editable first.
if fInfo.isLFSFile {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
} else if fInfo.isAnnexFile {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_annex_files")
} else if !isRepresentableAsText {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
}
@ -546,6 +604,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
},
Metas: metas,
GitRepo: ctx.Repo.GitRepo,
Blob: entry.Blob(),
}, rd)
if err != nil {
ctx.ServerError("Render", err)
@ -599,7 +658,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["FileContent"] = fileContent
ctx.Data["LineEscapeStatus"] = statuses
}
if !fInfo.isLFSFile {
if !fInfo.isLFSFile && !fInfo.isAnnexFile {
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.Data["CanEditFile"] = false
@ -644,6 +703,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
},
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Blob: entry.Blob(),
}, rd)
if err != nil {
ctx.ServerError("Render", err)
@ -1159,6 +1219,15 @@ PostRecentBranchCheck:
} else {
ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions
}
isAnnexFile, okAnnexFile := ctx.Data["IsAnnexFile"]
isAnnexFilePresent, okAnnexFilePresent := ctx.Data["IsAnnexFilePresent"]
if okAnnexFile && okAnnexFilePresent && isAnnexFile.(bool) && !isAnnexFilePresent.(bool) {
// If the file to be viewed is annexed but not present then render it normally
// (which will show the plain git blob content, i.e. the symlink or pointer target)
// but make the status code a 404.
ctx.HTML(http.StatusNotFound, tplRepoHome)
return
}
ctx.HTML(http.StatusOK, tplRepoHome)
}

View file

@ -356,6 +356,20 @@ func registerRoutes(m *web.Route) {
}
}
annexEnabled := func(ctx *context.Context) {
if !setting.Annex.Enabled {
ctx.Error(http.StatusNotFound)
return
}
}
annexP2PHTTPEnabled := func(ctx *context.Context) {
if setting.Annex.DisableP2PHTTP {
ctx.Error(http.StatusNotFound)
return
}
}
federationEnabled := func(ctx *context.Context) {
if !setting.Federation.Enabled {
ctx.Error(http.StatusNotFound)
@ -955,6 +969,9 @@ func registerRoutes(m *web.Route) {
// ***** END: Organization *****
// ***** START: Repository *****
m.Group("", func() {
m.Methods("GET,POST", "/git-annex-p2phttp/git-annex/{uuid}/*", repo.AnnexP2PHTTP)
}, ignSignInAndCsrf, annexEnabled, annexP2PHTTPEnabled)
m.Group("/repo", func() {
m.Get("/create", repo.Create)
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
@ -1635,6 +1652,12 @@ func registerRoutes(m *web.Route) {
})
}, ignSignInAndCsrf, lfsServerEnabled)
m.Group("", func() {
// for git-annex
m.Methods("GET,OPTIONS", "/config", repo.GetConfig) // needed by clients reading annex.uuid during `git annex initremote`
m.Methods("GET,OPTIONS", "/annex/objects/{hash1}/{hash2}/{keyDir}/{key}", repo.GetAnnexObject)
}, ignSignInAndCsrf, annexEnabled, context.UserAssignmentWeb())
gitHTTPRouters(m)
})
})

View file

@ -61,6 +61,17 @@ func isArchivePath(req *http.Request) bool {
return archivePathRe.MatchString(req.URL.Path)
}
var annexPathRe = regexp.MustCompile(`^(/git-annex-p2phttp/|/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/annex/)`)
func isAnnexPath(req *http.Request) bool {
if setting.Annex.Enabled {
// "/config" is git's config, not specifically git-annex's; but the only current
// user of it is when git-annex downloads the annex.uuid during 'git annex init'.
return strings.HasSuffix(req.URL.Path, "/config") || annexPathRe.MatchString(req.URL.Path)
}
return false
}
// handleSignIn clears existing session variables and stores new ones for the specified user object
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
// We need to regenerate the session...

View file

@ -43,8 +43,8 @@ func (b *Basic) Name() string {
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
// Basic authentication should only fire on API, Download or on Git, LFSPaths or Git-Annex paths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) && !isAnnexPath(req) {
return nil, nil
}

61
services/pull/annex.go Normal file
View file

@ -0,0 +1,61 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"io"
"sync"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/git/pipeline"
)
// AnnexPush copies all annexed files referenced in new commits from the head repository to the base repository
func AnnexPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string) error {
// Initialize the temporary repository with git-annex
if err := annex.PrivateInit(ctx, tmpBasePath); err != nil {
return err
}
revListReader, revListWriter := io.Pipe()
shasToCheckReader, shasToCheckWriter := io.Pipe()
catFileCheckReader, catFileCheckWriter := io.Pipe()
shasToBatchReader, shasToBatchWriter := io.Pipe()
lookupKeyBatchReader, lookupKeyBatchWriter := io.Pipe()
errChan := make(chan error, 1)
wg := sync.WaitGroup{}
wg.Add(6)
// Create the go-routines in reverse order.
// 6. Take the referenced keys and copy their data from the head repository to
// the base repository
go annex.CopyFromToBatch(ctx, "head_repo", "origin", lookupKeyBatchReader, &wg, tmpBasePath)
// 5. Take the shas of the blobs and resolve them to annex keys, git-annex
// should filter out anything that doesn't reference a key
go annex.LookupKeyBatch(ctx, shasToBatchReader, lookupKeyBatchWriter, &wg, tmpBasePath)
// 4. From the provided objects restrict to blobs <=32KiB
go pipeline.BlobsLessThanOrEqual32KiBFromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
// 3. Run batch-check on the objects retrieved from rev-list
go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, tmpBasePath)
// 2. Check each object retrieved rejecting those without names as they will be commits or trees
go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
// 1. Run rev-list objects from mergeHead to mergeBase
go pipeline.RevListObjects(ctx, revListWriter, &wg, tmpBasePath, mergeHeadSHA, mergeBaseSHA, errChan)
wg.Wait()
select {
case err, has := <-errChan:
if has {
return err
}
default:
}
return nil
}

View file

@ -22,6 +22,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@ -314,6 +315,12 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
}
}
if setting.Annex.Enabled && annex.PathIsAnnexRepo(pr.BaseRepo.RepoPath()) && annex.PathIsAnnexRepo(pr.HeadRepo.RepoPath()) {
if err := AnnexPush(ctx, mergeCtx.tmpBasePath, mergeHeadSHA, mergeBaseSHA); err != nil {
return "", err
}
}
var headUser *user_model.User
err = pr.HeadRepo.LoadOwner(ctx)
if err != nil {

View file

@ -202,6 +202,26 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat
return nil
}
// InitPrivateAnnex initializes a private annex in the repository
func (t *TemporaryUploadRepository) InitPrivateAnnex() error {
if _, _, err := git.NewCommand(t.ctx, "config", "annex.private", "true").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
return err
}
if _, _, err := git.NewCommand(t.ctx, "annex", "init").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
return err
}
return nil
}
// AddAnnex adds the file at path to the repository using git annex add
// This requires a non-bare repository
func (t *TemporaryUploadRepository) AddAnnex(path string) error {
if _, _, err := git.NewCommand(t.ctx, "annex", "add").AddDynamicArguments(path).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
return err
}
return nil
}
// WriteTree writes the current index as a tree to the object db and returns its hash
func (t *TemporaryUploadRepository) WriteTree() (string, error) {
stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})

View file

@ -6,13 +6,16 @@ package files
import (
"context"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
@ -89,7 +92,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
defer t.Close()
hasOldBranch := true
if err = t.Clone(opts.OldBranch, true); err != nil {
if err = t.Clone(opts.OldBranch, false); err != nil {
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return err
}
@ -105,10 +108,30 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
}
// Copy uploaded files into repository.
if err := copyUploadedLFSFilesIntoRepository(infos, t, opts.TreePath); err != nil {
r, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return err
}
if annex.IsAnnexRepo(r) {
// Initialize annex privately in temporary clone
if err := t.InitPrivateAnnex(); err != nil {
return err
}
// Copy uploaded files into git-annex repository
if err := copyUploadedFilesIntoAnnexRepository(infos, t, opts.TreePath); err != nil {
return err
}
// Move all annexed content in the temporary repository, i.e. everything we have just added, to the origin
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
if err := moveAnnexedFilesToOrigin(t, author, committer); err != nil {
return err
}
} else {
// Copy uploaded files into repository.
if err := copyUploadedLFSFilesIntoRepository(infos, t, opts.TreePath); err != nil {
return err
}
}
// Now write the tree
treeHash, err := t.WriteTree()
@ -246,3 +269,57 @@ func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) er
}
return nil
}
func copyUploadedFilesIntoAnnexRepository(infos []uploadInfo, t *TemporaryUploadRepository, treePath string) error {
for i := range len(infos) {
if err := copyUploadedFileIntoAnnexRepository(&infos[i], t, treePath); err != nil {
return err
}
}
return nil
}
func copyUploadedFileIntoAnnexRepository(info *uploadInfo, t *TemporaryUploadRepository, treePath string) error {
pathInRepo := path.Join(t.basePath, treePath, info.upload.Name)
if err := os.MkdirAll(filepath.Dir(pathInRepo), 0o700); err != nil {
return err
}
if err := os.Rename(info.upload.LocalPath(), pathInRepo); err != nil {
// Rename didn't work, try copy and remove
inputFile, err := os.Open(info.upload.LocalPath())
if err != nil {
return fmt.Errorf("could not open source file: %v", err)
}
defer inputFile.Close()
outputFile, err := os.Create(pathInRepo)
if err != nil {
return fmt.Errorf("could not open dest file: %v", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return fmt.Errorf("could not copy to dest from source: %v", err)
}
inputFile.Close()
err = os.Remove(info.upload.LocalPath())
if err != nil {
return fmt.Errorf("could not remove source file: %v", err)
}
}
return t.AddAnnex(pathInRepo)
}
func moveAnnexedFilesToOrigin(t *TemporaryUploadRepository, author, committer *user_model.User) error {
authorSig := author.NewGitSig()
committerSig := committer.NewGitSig()
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
"GIT_AUTHOR_EMAIL="+authorSig.Email,
"GIT_COMMITTER_NAME="+committerSig.Name,
"GIT_COMMITTER_EMAIL="+committerSig.Email,
)
if _, _, err := git.NewCommand(t.ctx, "annex", "move", "--to", "origin").RunStdString(&git.RunOpts{Dir: t.basePath, Env: env}); err != nil {
return err
}
return nil
}

View file

@ -17,6 +17,7 @@
{{if .FileSize}}
<div class="file-info-entry">
{{ctx.Locale.TrSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
{{if .IsAnnexFile}} ({{ctx.Locale.Tr "repo.stored_annex"}}{{if not .IsAnnexFilePresent}} - {{ctx.Locale.Tr "repo.stored_annex_not_present"}}{{end}}){{end}}
</div>
{{end}}
{{if .LFSLock}}

View file

@ -22,6 +22,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/forms"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -461,3 +462,27 @@ func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, r
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}
// generate and activate an ssh key for the user attached to the APITestContext
// TODO: pick a better name; golang doesn't do method overriding.
func withCtxKeyFile(t *testing.T, ctx APITestContext, callback func()) {
// we need to have write:public_key to do this step
// the easiest way is to create a throwaway ctx that is identical but only has that permission
ctxKeyWriter := ctx
ctxKeyWriter.Token = getTokenForLoggedInUser(t, ctx.Session, auth.AccessTokenScopeWriteUser)
keyName := "One of " + ctx.Username + "'s keys: #" + uuid.New().String()
withKeyFile(t, keyName, func(keyFile string) {
var key api.PublicKey
doAPICreateUserKey(ctxKeyWriter, keyName, keyFile,
func(t *testing.T, _key api.PublicKey) {
// save the key ID so we can delete it at the end
key = _key
})(t)
defer doAPIDeleteUserKey(ctxKeyWriter, key.ID)(t)
callback()
})
}

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,28 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
"ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700)
require.NoError(t, err)
// reset ssh wrapper afterwards
_gitSSH, gitSSHExists := os.LookupEnv("GIT_SSH")
defer func() {
if gitSSHExists {
os.Setenv("GIT_SSH", _gitSSH)
}
}()
_gitSSHCommand, gitSSHCommandExists := os.LookupEnv("GIT_SSH_COMMAND")
defer func() {
if gitSSHCommandExists {
os.Setenv("GIT_SSH_COMMAND", _gitSSHCommand)
}
}()
_gitSSHVariant, gitSSHVariantExists := os.LookupEnv("GIT_SSH_VARIANT")
defer func() {
if gitSSHVariantExists {
os.Setenv("GIT_SSH_VARIANT", _gitSSHVariant)
}
}()
// Setup ssh wrapper
t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
t.Setenv("GIT_SSH_COMMAND",
@ -51,6 +73,13 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
callback(keyFile)
}
func createHTTPUrl(gitPath string, u *url.URL) *url.URL {
// this assumes u contains the HTTP base URL that Gitea is running on
u2 := *u
u2.Path = gitPath
return &u2
}
func createSSHUrl(gitPath string, u *url.URL) *url.URL {
u2 := *u
u2.Scheme = "ssh"

View file

@ -97,6 +97,9 @@ DISABLE_QUERY_AUTH_TOKEN = true
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
[annex]
ENABLED = true
[packages]
ENABLED = true

View file

@ -122,6 +122,9 @@ MINIO_LOCATION = us-east-1
MINIO_USE_SSL = false
MINIO_CHECKSUM_ALGORITHM = md5
[annex]
ENABLED = true
[packages]
ENABLED = true

View file

@ -102,6 +102,9 @@ JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
[annex]
ENABLED = true
[packages]
ENABLED = true

View file

@ -5,9 +5,11 @@
package tests
import (
"bytes"
"context"
"database/sql"
"fmt"
"io"
"os"
"path"
"path/filepath"
@ -488,3 +490,80 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
return CreateDeclarativeRepoWithOptions(t, owner, opts)
}
// Decide if two files have the same contents or not.
// chunkSize is the size of the blocks to scan by; pass 0 to get a sensible default.
// *Follows* symlinks.
//
// May return an error if something else goes wrong; in this case, you should ignore the value of 'same'.
//
// derived from https://stackoverflow.com/a/30038571
// under CC-BY-SA-4.0 by several contributors
func FileCmp(file1, file2 string, chunkSize int) (same bool, err error) {
if chunkSize == 0 {
chunkSize = 4 * 1024
}
// shortcuts: check file metadata
stat1, err := os.Stat(file1)
if err != nil {
return false, err
}
stat2, err := os.Stat(file2)
if err != nil {
return false, err
}
// are inputs are literally the same file?
if os.SameFile(stat1, stat2) {
return true, nil
}
// do inputs at least have the same size?
if stat1.Size() != stat2.Size() {
return false, nil
}
// long way: compare contents
f1, err := os.Open(file1)
if err != nil {
return false, err
}
defer f1.Close()
f2, err := os.Open(file2)
if err != nil {
return false, err
}
defer f2.Close()
b1 := make([]byte, chunkSize)
b2 := make([]byte, chunkSize)
for {
n1, err1 := io.ReadFull(f1, b1)
n2, err2 := io.ReadFull(f2, b2)
// https://pkg.go.dev/io#Reader
// > Callers should always process the n > 0 bytes returned
// > before considering the error err. Doing so correctly
// > handles I/O errors that happen after reading some bytes
// > and also both of the allowed EOF behaviors.
if !bytes.Equal(b1[:n1], b2[:n2]) {
return false, nil
}
if (err1 == io.EOF && err2 == io.EOF) || (err1 == io.ErrUnexpectedEOF && err2 == io.ErrUnexpectedEOF) {
return true, nil
}
// some other error, like a dropped network connection or a bad transfer
if err1 != nil {
return false, err1
}
if err2 != nil {
return false, err2
}
}
}

View file

@ -244,6 +244,7 @@ td .commit-summary {
}
.repository.file.list #repo-files-table tbody .svg.octicon-file,
.repository.file.list #repo-files-table tbody .svg.octicon-file-binary,
.repository.file.list #repo-files-table tbody .svg.octicon-file-symlink-file,
.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-symlink {
color: var(--color-secondary-dark-7);

View file

@ -92,7 +92,17 @@ export function initImageDiff() {
return loadElem(img, info.path);
}));
// only the first images is associated with $boundsInfo
if (!success) info.$boundsInfo.text('(image error)');
if (!success) {
const blobContent = await GET(info.path.replace('/media/', '/raw/')).then((response) => response.text());
if (blobContent.startsWith('.git/annex/objects')) {
for (const item of document.querySelectorAll('.image-diff .overflow-menu-items .item')) {
item.style.display = 'none';
}
info.$boundsInfo[0].parentElement.textContent = 'annexed file is not present on the server';
} else {
info.$boundsInfo.text('(image error)');
}
}
if (info.mime === 'image/svg+xml') {
const resp = await GET(info.path);
const text = await resp.text();