diff --git a/.forgejo/workflows-composite/apt-install-from/action.yaml b/.forgejo/workflows-composite/apt-install-from/action.yaml
index 615e7cb..ab55883 100644
--- a/.forgejo/workflows-composite/apt-install-from/action.yaml
+++ b/.forgejo/workflows-composite/apt-install-from/action.yaml
@@ -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}}
diff --git a/.forgejo/workflows-composite/setup-env/action.yaml b/.forgejo/workflows-composite/setup-env/action.yaml
index 28216e9..f19569a 100644
--- a/.forgejo/workflows-composite/setup-env/action.yaml
+++ b/.forgejo/workflows-composite/setup-env/action.yaml
@@ -19,7 +19,7 @@ runs:
set -ex
toolchain=$(grep -oP '(?<=toolchain ).+' go.mod)
version=$(go version | cut -d' ' -f3)
- if [ "$toolchain" != "$version" ]; then
- echo "go version mismatch: $toolchain <> $version"
+ if dpkg --compare-versions ${version#go} lt ${toolchain#go}; then
+ echo "go version too low: $toolchain >= $version"
exit 1
fi
diff --git a/.forgejo/workflows/build-oci-image.yml b/.forgejo/workflows/build-oci-image.yml
new file mode 100644
index 0000000..8e843b4
--- /dev/null
+++ b/.forgejo/workflows/build-oci-image.yml
@@ -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') }}
diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml
index eb3163d..c4972c9 100644
--- a/.forgejo/workflows/testing.yml
+++ b/.forgejo/workflows/testing.yml
@@ -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
diff --git a/Dockerfile b/Dockerfile
index ae21a08..d39de78 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -78,6 +78,7 @@ RUN apk --no-cache add \
sqlite \
su-exec \
gnupg \
+ git-annex \
&& rm -rf /var/cache/apk/*
RUN addgroup \
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index c5d6a13..d636e10 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -71,6 +71,7 @@ RUN apk --no-cache add \
git \
curl \
gnupg \
+ git-annex \
&& rm -rf /var/cache/apk/*
RUN addgroup \
diff --git a/Makefile b/Makefile
index a9de57e..561d674 100644
--- a/Makefile
+++ b/Makefile
@@ -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/\..*//')
diff --git a/cmd/serv.go b/cmd/serv.go
index db67e36..5780403 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -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)
diff --git a/cmd/web.go b/cmd/web.go
index 44babd5..661e6d1 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -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:
diff --git a/modules/annex/annex.go b/modules/annex/annex.go
new file mode 100644
index 0000000..dee24d2
--- /dev/null
+++ b/modules/annex/annex.go
@@ -0,0 +1,192 @@
+// 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 (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
+)
+
+// ErrBlobIsNotAnnexed occurs if a blob does not contain a valid annex key
+var ErrBlobIsNotAnnexed = errors.New("not a git-annex pointer")
+
+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
+}
+
+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
+}
+
+// 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 repoConfigFileRe = regexp.MustCompile("[^/]+/[^/]+.git/config$")
+
+var (
+ uuid2repoPathCache = make(map[string]string)
+ repoPath2uuidCache = make(map[string]string)
+)
+
+func Init() error {
+ if !setting.Annex.Enabled {
+ return nil
+ }
+ log.Info("Populating the git-annex UUID cache with existing repositories")
+ return updateUUID2RepoPathCache()
+}
+
+func updateUUID2RepoPathCache() error {
+ return filepath.WalkDir(setting.RepoRootPath, func(path string, d fs.DirEntry, err error) error {
+ if err == nil && repoConfigFileRe.MatchString(path) {
+ thisRepoPath := strings.TrimSuffix(path, "/config")
+ _, ok := repoPath2uuidCache[thisRepoPath]
+ if ok {
+ return nil
+ }
+ stdout, _, err := git.NewCommand(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: thisRepoPath})
+ if err != nil {
+ return nil
+ }
+ repoUUID := strings.TrimSpace(stdout)
+ if repoUUID != "" {
+ uuid2repoPathCache[repoUUID] = thisRepoPath
+ repoPath2uuidCache[thisRepoPath] = 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)
+}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 02f1db5..a885546 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -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()
diff --git a/modules/git/blob.go b/modules/git/blob.go
index 2f02693..bbfab7d 100644
--- a/modules/git/blob.go
+++ b/modules/git/blob.go
@@ -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
diff --git a/modules/git/command.go b/modules/git/command.go
index a3d43aa..d3e6b7b 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -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
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index 122517e..b976077 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -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
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 2137302..c00bd2b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -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
diff --git a/modules/private/serv.go b/modules/private/serv.go
index 480a446..6c7c753 100644
--- a/modules/private/serv.go
+++ b/modules/private/serv.go
@@ -40,6 +40,7 @@ type ServCommandResults struct {
UserName string
UserEmail string
UserID int64
+ UserMode perm.AccessMode
OwnerName string
RepoName string
RepoID int64
diff --git a/modules/setting/annex.go b/modules/setting/annex.go
new file mode 100644
index 0000000..35e9e55
--- /dev/null
+++ b/modules/setting/annex.go
@@ -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
+ }
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index c9d3083..9710fb2 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -153,6 +153,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadCamoFrom(cfg)
loadI18nFrom(cfg)
loadGitFrom(cfg)
+ loadAnnexFrom(cfg)
loadMirrorFrom(cfg)
loadMarkupFrom(cfg)
loadQuotaFrom(cfg)
diff --git a/modules/util/remove.go b/modules/util/remove.go
index d1e38fa..39556e5 100644
--- a/modules/util/remove.go
+++ b/modules/util/remove.go
@@ -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
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 7d8fdee..e050e0d 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -1315,6 +1315,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
@@ -1340,6 +1341,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
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 6a479b7..22dd5f3 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -1315,6 +1315,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
@@ -1338,6 +1340,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
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index cb99563..3cbf24c 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -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=Το αρχείο είναι κλειδωμένο
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d2f47ad..45188fe 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1336,6 +1336,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
@@ -1363,6 +1365,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
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 4f9d141..b55ff31 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -1310,6 +1310,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
@@ -1333,6 +1334,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
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 8960be8..3d5be27 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -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=پرونده قفل شده است
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 2585eb1..06d5945 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1316,6 +1316,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
@@ -1341,6 +1342,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é
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index 57555b9..ef94d2d 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -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
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 1e0044e..8cf3457 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -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
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 3a6e844..b3cf17f 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -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ð
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 2f5bf6f..1e4aeab 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -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
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index b0fc38d..4ea4331 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -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=ファイルはロックされています
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index a2a50d7..72b4ed3 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -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=새 파일
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index c179158..9610867 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -1314,6 +1314,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
@@ -1337,6 +1338,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
@@ -4023,4 +4025,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.
\ No newline at end of file
+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.
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index c884160..3f89d43 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -1283,6 +1283,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
@@ -1301,6 +1302,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
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 2af7648..51bdca6 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -1148,6 +1148,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
@@ -1165,6 +1166,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
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index d826e60..d85e002 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -1308,6 +1308,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
@@ -1331,6 +1332,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
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 309fdbe..23158b8 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1319,6 +1319,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
@@ -1344,6 +1345,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
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index ec844fa..18c30e4 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -1302,6 +1302,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=Граф коммитов
@@ -1325,6 +1326,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=Файл заблокирован
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index ac7627c..63c728c 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -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=ගොනුවට අගුළු ලා ඇත
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index bd2ce20..cfb3d76 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -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
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 928d4a3..7358e5a 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -891,6 +891,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
@@ -904,6 +905,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
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 82476d4..8890f6d 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -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
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 2186f44..c92ac9d 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -1218,6 +1218,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=Виберіть гілки
@@ -1235,6 +1236,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=Файл заблоковано
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 7354a4d..62021be 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1317,6 +1317,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
@@ -1342,6 +1343,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=文件已锁定
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index e5080e6..25dfcdf 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -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]
\ No newline at end of file
+[git.filemode]
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index 5caafac..3dcce0b 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -1263,6 +1263,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=選擇分支
@@ -1282,6 +1283,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=檔案已被鎖定
diff --git a/package-lock.json b/package-lock.json
index e081796..c5f1950 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "forgejo",
+ "name": "forgejo-aneksajo",
"lockfileVersion": 3,
"requires": true,
"packages": {
diff --git a/routers/init.go b/routers/init.go
index 821a0ef..d881edd 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -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)
}
diff --git a/routers/private/serv.go b/routers/private/serv.go
index ef3920d..a3a0c69 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -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)
diff --git a/routers/web/repo/annex.go b/routers/web/repo/annex.go
new file mode 100644
index 0000000..852b5a1
--- /dev/null
+++ b/routers/web/repo/annex.go
@@ -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)
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 03d49fa..104655e 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -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)
}
}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 1e87bbf..aefaa79 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -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)
}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index c1adca1..53f18f9 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -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)
+ }
+}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index fd8c1da..6329f5d 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -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)
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 4d8d280..db0015f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -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)
})
})
diff --git a/services/auth/auth.go b/services/auth/auth.go
index c108723..ddd3191 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -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...
diff --git a/services/auth/basic.go b/services/auth/basic.go
index d489164..8e8fbfc 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -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
}
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 6e7570b..566ae5f 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -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})
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index 1330116..21cd5a8 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -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
+}
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 6ae7c15..8655404 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -17,6 +17,7 @@
{{if .FileSize}}
{{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}}
{{end}}
{{if .LFSLock}}
diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go
index dae71ca..5bd8dc5 100644
--- a/tests/integration/api_helper_for_declarative_test.go
+++ b/tests/integration/api_helper_for_declarative_test.go
@@ -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()
+ })
+}
diff --git a/tests/integration/git_annex_test.go b/tests/integration/git_annex_test.go
new file mode 100644
index 0000000..efcb571
--- /dev/null
+++ b/tests/integration/git_annex_test.go
@@ -0,0 +1,2925 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/annex"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/require"
+)
+
+// Some guidelines:
+//
+// * a APITestContext is an awkward union of session credential + username + target repo
+// which is assumed to be owned by that username; if you want to target a different
+// repo, you need to edit its .Reponame or just ignore it and write "username/reponame.git"
+
+func doCreateRemoteAnnexRepository(t *testing.T, u *url.URL, ctx APITestContext, private bool, objectFormat git.ObjectFormat) (err error) {
+ // creating a repo counts as editing the user's profile (is done by POSTing
+ // to /api/v1/user/repos/) -- which means it needs a User-scoped token and
+ // both that and editing need a Repo-scoped token because they edit repositories.
+ rescopedCtx := ctx
+ rescopedCtx.Token = getTokenForLoggedInUser(t, ctx.Session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
+ doAPICreateRepository(rescopedCtx, false, objectFormat)(t)
+ t.Cleanup(func() { util.MakeWritable(setting.RepoRootPath) })
+ doAPIEditRepository(rescopedCtx, &api.EditRepoOption{Private: &private})(t)
+
+ repoURL := createSSHUrl(ctx.GitPath(), u)
+
+ // Fill in fixture data
+ withAnnexCtxKeyFile(t, ctx, func() {
+ err = doInitRemoteAnnexRepository(t, repoURL)
+ })
+ if err != nil {
+ return fmt.Errorf("Unable to initialize remote repo with git-annex fixture: %w", err)
+ }
+ return nil
+}
+
+func TestGitAnnexWebUpload(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-web-upload-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ uploadFile := func(t *testing.T, path string) string {
+ t.Helper()
+
+ body := &bytes.Buffer{}
+ mpForm := multipart.NewWriter(body)
+ err := mpForm.WriteField("_csrf", GetCSRF(t, ctx.Session, ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch))
+ require.NoError(t, err)
+
+ file, err := mpForm.CreateFormFile("file", filepath.Base(path))
+ require.NoError(t, err)
+
+ srcFile, err := os.Open(path)
+ require.NoError(t, err)
+
+ io.Copy(file, srcFile)
+ require.NoError(t, mpForm.Close())
+
+ req := NewRequestWithBody(t, "POST", "/"+ctx.Username+"/"+ctx.Reponame+"/upload-file", body)
+ req.Header.Add("Content-Type", mpForm.FormDataContentType())
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ respMap := map[string]string{}
+ DecodeJSON(t, resp, &respMap)
+ return respMap["uuid"]
+ }
+
+ // Generate random file
+ tmpFile := path.Join(t.TempDir(), "web-upload-test-file.bin")
+ require.NoError(t, generateRandomFile(1024*1024/4, tmpFile))
+ expectedContent, err := os.ReadFile(tmpFile)
+ require.NoError(t, err)
+
+ // Upload generated file
+ fileUUID := uploadFile(t, tmpFile)
+ req := NewRequestWithValues(t, "POST", ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch, map[string]string{
+ "commit_choice": "direct",
+ "files": fileUUID,
+ "_csrf": GetCSRF(t, ctx.Session, ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch),
+ "commit_mail_id": "-1",
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Get some handles on the target repository and file
+ remoteRepoPath := path.Join(setting.RepoRootPath, ctx.GitPath())
+ repo, err := git.OpenRepository(git.DefaultContext, remoteRepoPath)
+ require.NoError(t, err)
+ defer repo.Close()
+ tree, err := repo.GetTree(setting.Repository.DefaultBranch)
+ require.NoError(t, err)
+ treeEntry, err := tree.GetTreeEntryByPath(filepath.Base(tmpFile))
+ require.NoError(t, err)
+ blob := treeEntry.Blob()
+
+ // Check that the uploaded file is annexed
+ isAnnexed, err := annex.IsAnnexed(blob)
+ require.NoError(t, err)
+ require.True(t, isAnnexed)
+
+ // Check that the uploaded file has the correct content
+ annexedFile, err := annex.Content(blob)
+ require.NoError(t, err)
+ actualContent, err := io.ReadAll(annexedFile)
+ require.NoError(t, err)
+ require.Equal(t, expectedContent, actualContent)
+ })
+ })
+}
+
+func TestGitAnnexMedia(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-media-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ // the filenames here correspond to specific cases defined in doInitAnnexRepository()
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doAnnexMediaTest(t, ctx, "annexed.tiff")
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doAnnexMediaTest(t, ctx, "annexed.bin")
+ })
+ })
+ })
+}
+
+func doAnnexMediaTest(t *testing.T, ctx APITestContext, file string) {
+ // Make sure that downloading via /media on the website recognizes it should give the annexed content
+
+ // TODO:
+ // - [ ] roll this into TestGitAnnexPermissions to ensure that permission enforcement works correctly even on /media?
+
+ session := loginUser(t, ctx.Username) // logs in to the http:// site/API, storing a cookie;
+ // this is a different auth method than the git+ssh:// or git+http:// protocols TestGitAnnexPermissions uses!
+
+ // compute server-side path of the annexed file
+ remoteRepoPath := path.Join(setting.RepoRootPath, ctx.GitPath())
+ remoteObjectPath, err := contentLocation(remoteRepoPath, file)
+ require.NoError(t, err)
+
+ // download annexed file
+ localObjectPath := path.Join(t.TempDir(), file)
+ fd, err := os.OpenFile(localObjectPath, os.O_CREATE|os.O_WRONLY, 0o777)
+ defer fd.Close()
+ require.NoError(t, err)
+
+ mediaLink := path.Join("/", ctx.Username, ctx.Reponame, "/media/branch/master", file)
+ req := NewRequest(t, "GET", mediaLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ _, err = io.Copy(fd, resp.Body)
+ require.NoError(t, err)
+ fd.Close()
+
+ // verify the download
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ require.NoError(t, err)
+ require.True(t, match, "Annexed files should be the same")
+}
+
+func TestGitAnnexViews(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-template-render-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ session := loginUser(t, ctx.Username)
+
+ t.Run("Index", func(t *testing.T) {
+ // test that annexed files render with the binary file icon on the main list
+ defer tests.PrintCurrentTest(t)()
+
+ repoLink := path.Join("/", ctx.Username, ctx.Reponame)
+ req := NewRequest(t, "GET", repoLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ isFileBinaryIconLocked := htmlDoc.Find("tr[data-entryname='annexed.tiff'] > td.name svg").HasClass("octicon-file-binary")
+ require.True(t, isFileBinaryIconLocked, "locked annexed files should render a binary file icon")
+ isFileBinaryIconUnlocked := htmlDoc.Find("tr[data-entryname='annexed.bin'] > td.name svg").HasClass("octicon-file-binary")
+ require.True(t, isFileBinaryIconUnlocked, "unlocked annexed files should render a binary file icon")
+ })
+
+ t.Run("View", func(t *testing.T) {
+ // test how routers/web/repo/view.go + templates/repo/view_file.tmpl handle annexed files
+ defer tests.PrintCurrentTest(t)()
+
+ doViewTest := func(file string) (htmlDoc *HTMLDoc, viewLink, mediaLink string) {
+ viewLink = path.Join("/", ctx.Username, ctx.Reponame, "/src/branch/master", file)
+ // rawLink := strings.Replace(viewLink, "/src/", "/raw/", 1) // TODO: do something with this?
+ mediaLink = strings.Replace(viewLink, "/src/", "/media/", 1)
+
+ req := NewRequest(t, "GET", viewLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ // the first button on the toolbar on the view template is the "Raw" button
+ // this CSS selector is the most precise I can think to use
+ buttonLink, exists := htmlDoc.Find(".file-header").Find("a[download]").Attr("href")
+ require.True(t, exists, "Download button should exist on the file header")
+ require.EqualValues(t, mediaLink, buttonLink, "Download link should use /media URL for annex files")
+
+ return htmlDoc, viewLink, mediaLink
+ }
+
+ t.Run("Binary", func(t *testing.T) {
+ // test that annexing a file renders the /media link in /src and NOT the /raw link
+ defer tests.PrintCurrentTest(t)()
+
+ doBinaryViewTest := func(file string) {
+ htmlDoc, _, mediaLink := doViewTest(file)
+
+ rawLink, exists := htmlDoc.Find("div.file-view > div.view-raw > a").Attr("href")
+ require.True(t, exists, "Download link should render instead of content because this is a binary file")
+ require.EqualValues(t, mediaLink, rawLink)
+ }
+
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doBinaryViewTest("annexed.tiff")
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doBinaryViewTest("annexed.bin")
+ })
+ })
+
+ t.Run("Text", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ doTextViewTest := func(file string) {
+ htmlDoc, _, _ := doViewTest(file)
+ require.True(t, htmlDoc.Find("div.file-view").Is(".code-view"), "should render as code")
+ }
+
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doTextViewTest("annexed.txt")
+
+ t.Run("Markdown", func(t *testing.T) {
+ // special case: check that markdown can be pulled out of the annex and rendered, too
+ defer tests.PrintCurrentTest(t)()
+ htmlDoc, _, _ := doViewTest("annexed.md")
+ require.True(t, htmlDoc.Find("div.file-view").Is(".markdown"), "should render as markdown")
+ })
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doTextViewTest("annexed.rst")
+
+ t.Run("Markdown", func(t *testing.T) {
+ // special case: check that markdown can be pulled out of the annex and rendered, too
+ defer tests.PrintCurrentTest(t)()
+ htmlDoc, _, _ := doViewTest("annexed.markdown")
+ require.True(t, htmlDoc.Find("div.file-view").Is(".markdown"), "should render as markdown")
+ })
+ })
+ })
+ })
+ })
+ })
+}
+
+/*
+Test that permissions are enforced on git-annex-shell commands.
+
+ Along the way, this also tests that uploading, downloading, and deleting all work,
+ so we haven't written separate tests for those.
+*/
+func TestGitAnnexPermissions(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ // Each case below is split so that 'clone' is done as
+ // the repo owner, but 'copy' as the user under test.
+ //
+ // Otherwise, in cases where permissions block the
+ // initial 'clone', the test would simply end there
+ // and never verify if permissions apply properly to
+ // 'annex copy' -- potentially leaving a security gap.
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ // Tell git-annex to allow http://127.0.0.1, http://localhost and http://::1. Without
+ // this, all `git annex` commands will silently fail when run against http:// remotes
+ // without explaining what's wrong.
+ //
+ // Note: onGiteaRun() sets up an alternate HOME so this actually edits
+ // tests/integration/gitea-integration-*/data/home/.gitconfig and
+ // if you're debugging you need to remember to match that.
+ _, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddArguments("annex.security.allowed-ip-addresses", "all").RunStdString(&git.RunOpts{})
+ require.NoError(t, err)
+
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ t.Run("Public", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ownerCtx := NewAPITestContext(t, "user2", "annex-public"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ownerCtx, false, objectFormat))
+
+ // double-check it's public
+ repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ownerCtx.Username, ownerCtx.Reponame)
+ require.NoError(t, err)
+ require.False(t, repo.IsPrivate)
+
+ remoteRepoPath := path.Join(setting.RepoRootPath, ownerCtx.GitPath()) // path on disk -- which can be examined directly because we're testing from localhost
+
+ // Different sessions, so we can test different permissions.
+ // We leave Reponame blank because we don't actually then later add it according to each case if needed
+ //
+ // NB: these usernames need to match appropriate entries in models/fixtures/user.yml
+ writerCtx := NewAPITestContext(t, "user5", "", auth_model.AccessTokenScopeWriteRepository)
+ readerCtx := NewAPITestContext(t, "user4", "", auth_model.AccessTokenScopeReadRepository)
+ outsiderCtx := NewAPITestContext(t, "user8", "", auth_model.AccessTokenScopeReadRepository) // a user with no specific access
+
+ // set up collaborators
+ doAPIAddCollaborator(ownerCtx, readerCtx.Username, perm.AccessModeRead)(t)
+ doAPIAddCollaborator(ownerCtx, writerCtx.Username, perm.AccessModeWrite)(t)
+
+ // tests
+ t.Run("Owner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Writer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Reader", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Outsider", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err = git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Only HTTP and P2PHTTP have an anonymous mode
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Delete the repo, make sure it's fully gone
+ doAPIDeleteRepository(ownerCtx)(t)
+ _, statErr := os.Stat(remoteRepoPath)
+ require.True(t, os.IsNotExist(statErr), "Remote annex repo should be removed from disk")
+ })
+ })
+
+ t.Run("Private", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ownerCtx := NewAPITestContext(t, "user2", "annex-private"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a private repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ownerCtx, true, objectFormat))
+
+ // double-check it's private
+ repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ownerCtx.Username, ownerCtx.Reponame)
+ require.NoError(t, err)
+ require.True(t, repo.IsPrivate)
+
+ remoteRepoPath := path.Join(setting.RepoRootPath, ownerCtx.GitPath()) // path on disk -- which can be examined directly because we're testing from localhost
+
+ // Different sessions, so we can test different permissions.
+ // We leave Reponame blank because we don't actually then later add it according to each case if needed
+ //
+ // NB: these usernames need to match appropriate entries in models/fixtures/user.yml
+ writerCtx := NewAPITestContext(t, "user5", "", auth_model.AccessTokenScopeWriteRepository)
+ readerCtx := NewAPITestContext(t, "user4", "", auth_model.AccessTokenScopeReadRepository)
+ outsiderCtx := NewAPITestContext(t, "user8", "", auth_model.AccessTokenScopeReadRepository) // a user with no specific access
+ // Note: there's also full anonymous access, which is only available for public HTTP repos;
+ // it should behave the same as 'outsider' but we (will) test it separately below anyway
+
+ // set up collaborators
+ doAPIAddCollaborator(ownerCtx, readerCtx.Username, perm.AccessModeRead)(t)
+ doAPIAddCollaborator(ownerCtx, writerCtx.Username, perm.AccessModeWrite)(t)
+
+ // tests
+ t.Run("Owner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Writer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Reader", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Outsider", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath), "annex init should fail due to permissions")
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath), "annex copy --from should fail due to permissions")
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "annex copy --to should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Try unsetting annexurl
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.Error(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Only HTTP and P2PHTTP have an anonymous mode
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ // Try unsetting annexurl
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.Error(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Delete the repo, make sure it's fully gone
+ doAPIDeleteRepository(ownerCtx)(t)
+ _, statErr := os.Stat(remoteRepoPath)
+ require.True(t, os.IsNotExist(statErr), "Remote annex repo should be removed from disk")
+ })
+ })
+ })
+ })
+}
+
+/*
+Test that 'git annex init' works.
+
+ precondition: repoPath contains a pre-cloned repo set up by doInitAnnexRepository().
+*/
+func doAnnexInitTest(remoteRepoPath, repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "init", "cloned-repo").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't `git annex init`: %w", err)
+ }
+
+ // - method 0: 'git config remote.origin.annex-uuid'.
+ // Demonstrates that 'git annex init' successfully contacted
+ // the remote git-annex and was able to learn its ID number.
+ readAnnexUUID, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config", "remote.origin.annex-uuid").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't read remote `git config remote.origin.annex-uuid`: %w", err)
+ }
+ readAnnexUUID = strings.TrimSpace(readAnnexUUID)
+
+ match := regexp.MustCompile("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$").MatchString(readAnnexUUID)
+ if !match {
+ return fmt.Errorf("'git config remote.origin.annex-uuid' should have been able to download the remote's uuid; but instead read '%s'", readAnnexUUID)
+ }
+
+ remoteAnnexUUID, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: remoteRepoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't read local `git config annex.uuid`: %w", err)
+ }
+
+ remoteAnnexUUID = strings.TrimSpace(remoteAnnexUUID)
+ match = regexp.MustCompile("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$").MatchString(remoteAnnexUUID)
+ if !match {
+ return fmt.Errorf("'git annex init' should have been able to download the remote's uuid; but instead read '%s'", remoteAnnexUUID)
+ }
+
+ if readAnnexUUID != remoteAnnexUUID {
+ return fmt.Errorf("'git annex init' should have read the expected annex UUID '%s', but instead got '%s'", remoteAnnexUUID, readAnnexUUID)
+ }
+
+ // - method 1: 'git annex whereis'.
+ // Demonstrates that git-annex understands annexed files can be found in the remote annex.
+ annexWhereis, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "whereis", "annexed.bin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't `git annex whereis`: %w", err)
+ }
+ // Note: this regex is unanchored because 'whereis' outputs multiple lines containing
+ // headers and 1+ remotes and we just want to find one of them.
+ match = regexp.MustCompile(regexp.QuoteMeta(remoteAnnexUUID) + " -- .* \\[origin\\]\n").MatchString(annexWhereis)
+ if !match {
+ return errors.New("'git annex whereis' should report files are known to be in [origin]")
+ }
+
+ return nil
+}
+
+func doAnnexTestremoteReadWriteTest(repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "testremote", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func doAnnexTestremoteReadOnlyTest(repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "testremote", "origin", "--test-readonly", "annexed.tiff").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func doAnnexDownloadTest(remoteRepoPath, repoPath string) (err error) {
+ // NB: this test does something slightly different if run separately from "doAnnexInitTest()":
+ // "git annex copy" will notice and run "git annex init", silently.
+ // This shouldn't change any results, but be aware in case it does.
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "copy", "--from", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // verify the files downloaded
+
+ cmp := func(filename string) error {
+ localObjectPath, err := contentLocation(repoPath, filename)
+ if err != nil {
+ return err
+ }
+ // localObjectPath := path.Join(repoPath, filename) // or, just compare against the checked-out file
+
+ remoteObjectPath, err := contentLocation(remoteRepoPath, filename)
+ if err != nil {
+ return err
+ }
+
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ if err != nil {
+ return err
+ }
+ if !match {
+ return errors.New("Annexed files should be the same")
+ }
+
+ return nil
+ }
+
+ // this is the annex-symlink file
+ stat, err := os.Lstat(path.Join(repoPath, "annexed.tiff"))
+ if err != nil {
+ return fmt.Errorf("Lstat: %w", err)
+ }
+ if !((stat.Mode() & os.ModeSymlink) != 0) {
+ // this line is really just double-checking that the text fixture is set up correctly
+ return errors.New("*.tiff should be a symlink")
+ }
+ if err = cmp("annexed.tiff"); err != nil {
+ return err
+ }
+
+ // this is the annex-pointer file
+ stat, err = os.Lstat(path.Join(repoPath, "annexed.bin"))
+ if err != nil {
+ return fmt.Errorf("Lstat: %w", err)
+ }
+ if !((stat.Mode() & os.ModeSymlink) == 0) {
+ // this line is really just double-checking that the text fixture is set up correctly
+ return errors.New("*.bin should not be a symlink")
+ }
+ err = cmp("annexed.bin")
+
+ return err
+}
+
+func doAnnexLocalDropTest(repoPath string) (err error) {
+ // This test assumes that files are present in repoPath, i.e. it is run after doAnnexDownloadTest.
+ // This test drops all files from the repository clone.
+ binPath, err := contentLocation(repoPath, "annexed.bin")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if err != nil {
+ return err
+ }
+ tiffPath, err := contentLocation(repoPath, "annexed.tiff")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(tiffPath)
+ if err != nil {
+ return err
+ }
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "drop").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.bin wasn't dropped properly: %w", err)
+ }
+ _, err = os.Stat(tiffPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.tiff wasn't dropped properly: %w", err)
+ }
+ return nil
+}
+
+func doAnnexUploadTest(remoteRepoPath, repoPath string) (err error) {
+ // NB: this test does something slightly different if run separately from "Init":
+ // it first runs "git annex init" silently in the background.
+ // This shouldn't change any results, but be aware in case it does.
+
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "contribution.bin"))
+ if err != nil {
+ return err
+ }
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex another file"})
+ if err != nil {
+ return err
+ }
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "copy", "--to", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // verify the file was uploaded
+ blob, err := blobForFile(repoPath, "contribution.bin")
+ if err != nil {
+ return err
+ }
+ key, err := annex.LookupKey(blob)
+ if err != nil {
+ return err
+ }
+ localObjectPath, err := annex.ContentLocationFromKey(repoPath, key)
+ if err != nil {
+ return err
+ }
+
+ remoteObjectPath, err := annex.ContentLocationFromKey(remoteRepoPath, key)
+ if err != nil {
+ return err
+ }
+
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ if err != nil {
+ return err
+ }
+ if !match {
+ return errors.New("Annexed files should be the same")
+ }
+
+ return nil
+}
+
+func doAnnexRemoteDropTest(remoteRepoPath, repoPath string) (err error) {
+ // This test assumes that files are present in repoPath, i.e. it is run after doAnnexDownloadTest.
+ // This test drops all files from the remote repository.
+ binPath, err := contentLocation(remoteRepoPath, "annexed.bin")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if err != nil {
+ return err
+ }
+ tiffPath, err := contentLocation(remoteRepoPath, "annexed.tiff")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(tiffPath)
+ if err != nil {
+ return err
+ }
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "drop", "--from", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.bin wasn't dropped properly: %w", err)
+ }
+ _, err = os.Stat(tiffPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.tiff wasn't dropped properly: %w", err)
+ }
+ return nil
+}
+
+// ---- Helpers ----
+
+func generateRandomFile(size int, path string) (err error) {
+ // Generate random file
+
+ // XXX TODO: maybe this should not be random, but instead a predictable pattern, so that the test is deterministic
+ bufSize := 4 * 1024
+ if bufSize > size {
+ bufSize = size
+ }
+
+ buffer := make([]byte, bufSize)
+
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ written := 0
+ for written < size {
+ n := size - written
+ if n > bufSize {
+ n = bufSize
+ }
+ _, err := rand.Read(buffer[:n])
+ if err != nil {
+ return err
+ }
+ n, err = f.Write(buffer[:n])
+ if err != nil {
+ return err
+ }
+ written += n
+ }
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ---- Annex-specific helpers ----
+
+/*
+Initialize a repo with some baseline annexed and non-annexed files.
+
+ TODO: perhaps this generator could be replaced with a fixture (see
+ integrations/gitea-repositories-meta/ and models/fixtures/repository.yml).
+ However we reuse this template for -different- repos, so maybe not.
+*/
+func doInitAnnexRepository(repoPath string) error {
+ // set up what files should be annexed
+ // in this case, all *.bin files will be annexed
+ // without this, git-annex's default config annexes every file larger than some number of megabytes
+ f, err := os.Create(path.Join(repoPath, ".gitattributes"))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // set up git-annex to store certain filetypes via *annex* pointers
+ // (https://git-annex.branchable.com/internals/pointer_file/).
+ // but only when run via 'git add' (see git-annex-smudge(1))
+ _, err = f.WriteString("* annex.largefiles=anything\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.bin filter=annex\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.rst filter=annex\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.markdown filter=annex\n")
+ if err != nil {
+ return err
+ }
+ f.Close()
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Configure git-annex settings"})
+ if err != nil {
+ return err
+ }
+
+ // 'git annex init'
+ err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "init", "test-repo").Run(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // add files to the annex, stored via annex symlinks
+ // // a binary file
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.tiff"))
+ if err != nil {
+ return err
+ }
+
+ // // a text file
+ err = os.WriteFile(path.Join(repoPath, "annexed.md"), []byte("Overview\n=====\n\n1. Profit\n2. ???\n3. Review Life Activations\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ // // a markdown file
+ err = os.WriteFile(path.Join(repoPath, "annexed.txt"), []byte("We're going to see the wizard\nThe wonderful\nMonkey of\nBoz\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "add", ".").Run(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // add files to the annex, stored via git-annex-smudge
+ // // a binary file
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.bin"))
+ if err != nil {
+ return err
+ }
+
+ // // a text file
+ err = os.WriteFile(path.Join(repoPath, "annexed.rst"), []byte("Title\n=====\n\n- this is to test annexing a text file\n- lists are fun\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ // // a markdown file
+ err = os.WriteFile(path.Join(repoPath, "annexed.markdown"), []byte("Overview\n=====\n\n1. Profit\n2. ???\n3. Review Life Activations\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+
+ // save everything
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex files"})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+/*
+Initialize a remote repo with some baseline annexed and non-annexed files.
+*/
+func doInitRemoteAnnexRepository(t *testing.T, repoURL *url.URL) error {
+ repoPath := path.Join(t.TempDir(), path.Base(repoURL.Path))
+ // This clone is immediately thrown away, which
+ // helps force the tests to be end-to-end.
+ defer util.RemoveAll(repoPath)
+
+ doGitClone(repoPath, repoURL)(t) // TODO: this call is the only reason for the testing.T; can it be removed?
+
+ err := doInitAnnexRepository(repoPath)
+ if err != nil {
+ return err
+ }
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "sync", "--content").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func blobForFile(repoPath, file string) (*git.Blob, error) {
+ repo, err := git.OpenRepository(git.DefaultContext, repoPath)
+ if err != nil {
+ return nil, err
+ }
+ defer repo.Close()
+
+ commitID, err := repo.GetRefCommitID("HEAD") // NB: to examine a *branch*, prefix with "refs/branch/", or call repo.GetBranchCommitID(); ditto for tags
+ if err != nil {
+ return nil, err
+ }
+
+ commit, err := repo.GetCommit(commitID)
+ if err != nil {
+ return nil, err
+ }
+
+ treeEntry, err := commit.GetTreeEntryByPath(file)
+ if err != nil {
+ return nil, err
+ }
+
+ return treeEntry.Blob(), nil
+}
+
+/*
+Find the path in .git/annex/objects/ of the contents for a given annexed file.
+
+ repoPath: the git repository to examine
+ file: the path (in the repo's current HEAD) of the annex pointer
+
+ TODO: pass a parameter to allow examining non-HEAD branches
+*/
+func contentLocation(repoPath, file string) (path string, err error) {
+ blob, err := blobForFile(repoPath, file)
+ if err != nil {
+ return "", err
+ }
+ return annex.ContentLocation(blob)
+}
+
+/* like withKeyFile(), but automatically sets it the account given in ctx for use by git-annex */
+func withAnnexCtxKeyFile(t *testing.T, ctx APITestContext, callback func()) {
+ _gitAnnexUseGitSSH, gitAnnexUseGitSSHExists := os.LookupEnv("GIT_ANNEX_USE_GIT_SSH")
+ defer func() {
+ // reset
+ if gitAnnexUseGitSSHExists {
+ t.Setenv("GIT_ANNEX_USE_GIT_SSH", _gitAnnexUseGitSSH)
+ }
+ }()
+
+ t.Setenv("GIT_ANNEX_USE_GIT_SSH", "1") // withKeyFile works by setting GIT_SSH_COMMAND, but git-annex only respects that if this is set
+
+ withCtxKeyFile(t, ctx, callback)
+}
+
+/*
+Like withKeyFile(), but sets HTTP credentials instead of SSH credentials.
+
+ It does this by temporarily arranging through `git config --global`
+ to use git-credential-store(1) with the password written to a tempfile.
+
+ This is the only reliable way to pass HTTP credentials non-interactively
+ to git-annex. See https://git-annex.branchable.com/bugs/http_remotes_ignore_annex.web-options_--netrc/#comment-b5a299e9826b322f2d85c96d4929a430
+ for joeyh's proclamation on the subject.
+
+ This **is only effective** when used around git.NewCommandContextNoGlobals() calls.
+ git.NewCommand() disables credential.helper as a precaution (see modules/git/git.go).
+
+ In contrast, the tests in git_test.go put the password in the remote's URL like
+ `git config remote.origin.url http://user2:password@localhost:3003/user2/repo-name.git`,
+ writing the password in repoPath+"/.git/config". That would be equally good, except
+ that git-annex ignores it!
+*/
+func withAnnexCtxHTTPPassword(t *testing.T, u *url.URL, ctx APITestContext, callback func()) {
+ credentialedURL := *u
+ credentialedURL.User = url.UserPassword(ctx.Username, userPassword) // NB: all test users use the same password
+
+ credentialedAnnexURL := *u
+ credentialedAnnexURL.Host = strings.ReplaceAll(credentialedAnnexURL.Host, "127.0.0.1", "localhost")
+ credentialedAnnexURL.Scheme = "annex+" + credentialedAnnexURL.Scheme
+ credentialedAnnexURL.Path += "git-annex-p2phttp"
+ credentialedAnnexURL.User = url.UserPassword(ctx.Username, userPassword) // NB: all test users use the same password
+
+ creds := path.Join(t.TempDir(), "creds")
+ require.NoError(t, os.WriteFile(creds, []byte(credentialedURL.String()+"\n"+credentialedAnnexURL.String()+"\n"), 0o600))
+
+ originalCredentialHelper, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global", "credential.helper").RunStdString(&git.RunOpts{})
+ if err != nil && !git.IsErrorExitCode(err, 1) {
+ // ignore the 'error' thrown when credential.helper is unset (when git config returns 1)
+ // but catch all others
+ require.NoError(t, err)
+ }
+ hasOriginalCredentialHelper := (err == nil)
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global", "credential.helper", fmt.Sprintf("store --file=%s", creds)).RunStdString(&git.RunOpts{})
+ require.NoError(t, err)
+
+ defer (func() {
+ // reset
+ if hasOriginalCredentialHelper {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddArguments("credential.helper").AddDynamicArguments(originalCredentialHelper).RunStdString(&git.RunOpts{})
+ } else {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddOptionValues("--unset").AddArguments("credential.helper").RunStdString(&git.RunOpts{})
+ }
+ require.NoError(t, err)
+ })()
+
+ callback()
+}
diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go
index 83d8177..575f01d 100644
--- a/tests/integration/git_helper_for_declarative_test.go
+++ b/tests/integration/git_helper_for_declarative_test.go
@@ -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"
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index e15e799..b832026 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -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
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index 340531f..781508a 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -122,6 +122,9 @@ MINIO_LOCATION = us-east-1
MINIO_USE_SSL = false
MINIO_CHECKSUM_ALGORITHM = md5
+[annex]
+ENABLED = true
+
[packages]
ENABLED = true
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
index 277916a..231c0d1 100644
--- a/tests/sqlite.ini.tmpl
+++ b/tests/sqlite.ini.tmpl
@@ -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
diff --git a/tests/test_utils.go b/tests/test_utils.go
index b3c03a3..6e9374d 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -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
+ }
+ }
+}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index e9cfc1d..651c69c 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -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);
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
index d1b139f..52c9017 100644
--- a/web_src/js/features/imagediff.js
+++ b/web_src/js/features/imagediff.js
@@ -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();