mirror of
https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo.git
synced 2025-04-22 06:06:45 +02:00
Compare commits
45 commits
forgejo
...
v10.0.1-gi
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6c61f80d21 | ||
![]() |
1298a315bd | ||
![]() |
c5b3812553 | ||
![]() |
0ee62840ff | ||
![]() |
efe77599e6 | ||
![]() |
22855c6238 | ||
![]() |
aa683cb1e2 | ||
![]() |
99d2de8956 | ||
![]() |
4b75d40853 | ||
![]() |
aa6674de6e | ||
![]() |
971f6f7bab | ||
![]() |
1dd3a341a7 | ||
![]() |
592b23f5cd | ||
![]() |
4d65d593ad | ||
![]() |
3cf2111dbf | ||
![]() |
cdc80b9fe6 | ||
![]() |
8065657151 | ||
![]() |
78ff09f63d | ||
![]() |
4c60dfb6f0 | ||
![]() |
5b2ae61a12 | ||
![]() |
e83f575da5 | ||
![]() |
246131cc69 | ||
![]() |
e97e20427d | ||
![]() |
5ff7951560 | ||
![]() |
56a9ea1fc3 | ||
![]() |
82c9a86e83 | ||
![]() |
2845a75cbe | ||
![]() |
2c0cb04776 | ||
![]() |
635770f8a4 | ||
![]() |
0831aeccd1 | ||
![]() |
0c81519d4e | ||
![]() |
3c11eb1e26 | ||
![]() |
24264cb45f | ||
![]() |
f0da2f268a | ||
![]() |
d8bf745749 | ||
![]() |
f42432a9e4 | ||
![]() |
cc193ec4ef | ||
![]() |
3d7458b8cc | ||
![]() |
37a8fa9b5c | ||
![]() |
d13faeccaa | ||
![]() |
57de00e5b9 | ||
![]() |
52485edf64 | ||
![]() |
1d01b58227 | ||
![]() |
2771c2e591 | ||
![]() |
3c7f598230 |
72 changed files with 4309 additions and 66 deletions
|
@ -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}}
|
||||
|
|
41
.forgejo/workflows/build-oci-image.yml
Normal file
41
.forgejo/workflows/build-oci-image.yml
Normal file
|
@ -0,0 +1,41 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'forgejo'
|
||||
tags:
|
||||
- '*-git-annex*'
|
||||
|
||||
jobs:
|
||||
build-oci-image:
|
||||
runs-on: docker
|
||||
strategy:
|
||||
matrix:
|
||||
type: ["rootful", "rootless"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # fetch the full history so that the Forgejo version is determined properly
|
||||
- name: Determine registry and username
|
||||
id: determine-registry-and-username
|
||||
run: |
|
||||
echo "registry=${GITHUB_SERVER_URL#https://}" >> "$GITHUB_OUTPUT"
|
||||
echo "username=${GITHUB_REPOSITORY%/*}" >> "$GITHUB_OUTPUT"
|
||||
- name: Install Docker
|
||||
run: curl -fsSL https://get.docker.com | sh
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ steps.determine-registry-and-username.outputs.registry }}
|
||||
username: ${{ steps.determine-registry-and-username.outputs.username }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ (matrix.type == 'rootful' && 'Dockerfile') || (matrix.type == 'rootless' && 'Dockerfile.rootless') }}
|
||||
push: true
|
||||
tags: ${{ steps.determine-registry-and-username.outputs.registry }}/${{ github.repository }}:${{ github.ref_name }}${{ (matrix.type == 'rootful' && ' ') || (matrix.type == 'rootless' && '-rootless') }}
|
|
@ -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
|
||||
|
|
|
@ -78,6 +78,7 @@ RUN apk --no-cache add \
|
|||
sqlite \
|
||||
su-exec \
|
||||
gnupg \
|
||||
git-annex \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
RUN addgroup \
|
||||
|
|
|
@ -71,6 +71,7 @@ RUN apk --no-cache add \
|
|||
git \
|
||||
curl \
|
||||
gnupg \
|
||||
git-annex \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
RUN addgroup \
|
||||
|
|
4
Makefile
4
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/\..*//')
|
||||
|
|
76
cmd/serv.go
76
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)
|
||||
|
|
11
cmd/web.go
11
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:
|
||||
|
|
|
@ -2678,6 +2678,17 @@ LEVEL = Info
|
|||
;; Limit the number of concurrent upload/download operations within a batch
|
||||
;BATCH_OPERATION_CONCURRENCY = 8
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[annex]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Whether git-annex is enabled; defaults to false
|
||||
;ENABLED = false
|
||||
;; Whether to disable p2phttp support; default is the same as repository.DISABLE_HTTP_GIT
|
||||
;DISABLE_P2PHTTP = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; settings for packages, will override storage setting
|
||||
|
|
256
modules/annex/annex.go
Normal file
256
modules/annex/annex.go
Normal file
|
@ -0,0 +1,256 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Unlike modules/lfs, which operates mainly on git.Blobs, this operates on git.TreeEntrys.
|
||||
// The motivation for this is that TreeEntrys have an easy pointer to the on-disk repo path,
|
||||
// while blobs do not (in fact, if building with TAGS=gogit, blobs might exist only in a mock
|
||||
// filesystem, living only in process RAM). We must have the on-disk path to do anything
|
||||
// useful with git-annex because all of its interesting data is on-disk under .git/annex/.
|
||||
|
||||
package annex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
|
||||
"gopkg.in/ini.v1" //nolint:depguard // This import is forbidden in favor of using the setting module, but we need ini parsing for something other than Forgejo settings
|
||||
)
|
||||
|
||||
// ErrBlobIsNotAnnexed occurs if a blob does not contain a valid annex key
|
||||
var ErrBlobIsNotAnnexed = errors.New("not a git-annex pointer")
|
||||
|
||||
func PrivateInit(ctx context.Context, repoPath string) error {
|
||||
if _, _, err := git.NewCommand(ctx, "config", "annex.private", "true").RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := git.NewCommand(ctx, "annex", "init").RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LookupKey(blob *git.Blob) (string, error) {
|
||||
stdout, _, err := git.NewCommand(git.DefaultContext, "annex", "lookupkey", "--ref").AddDynamicArguments(blob.ID.String()).RunStdString(&git.RunOpts{Dir: blob.Repo().Path})
|
||||
if err != nil {
|
||||
return "", ErrBlobIsNotAnnexed
|
||||
}
|
||||
key := strings.TrimSpace(stdout)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// LookupKeyBatch runs git annex lookupkey --batch --ref
|
||||
func LookupKeyBatch(ctx context.Context, shasToBatchReader *io.PipeReader, lookupKeyBatchWriter *io.PipeWriter, wg *sync.WaitGroup, repoPath string) {
|
||||
defer wg.Done()
|
||||
defer shasToBatchReader.Close()
|
||||
defer lookupKeyBatchWriter.Close()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
if err := git.NewCommand(ctx, "annex", "lookupkey", "--batch", "--ref").Run(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: lookupKeyBatchWriter,
|
||||
Stdin: shasToBatchReader,
|
||||
Stderr: stderr,
|
||||
}); err != nil {
|
||||
_ = lookupKeyBatchWriter.CloseWithError(fmt.Errorf("git annex lookupkey --batch --ref [%s]: %w - %s", repoPath, err, errbuf.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// CopyFromToBatch runs git -c annex.hardlink=true annex copy --batch-keys --from <remote> --to <remote>
|
||||
func CopyFromToBatch(ctx context.Context, from, to string, keysToCopyReader *io.PipeReader, wg *sync.WaitGroup, repoPath string) {
|
||||
defer wg.Done()
|
||||
defer keysToCopyReader.Close()
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
var errbuf strings.Builder
|
||||
if err := git.NewCommand(ctx, "-c", "annex.hardlink=true", "annex", "copy", "--batch-keys", "--from").AddDynamicArguments(from).AddArguments("--to").AddDynamicArguments(to).Run(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: stdout,
|
||||
Stdin: keysToCopyReader,
|
||||
Stderr: stderr,
|
||||
}); err != nil {
|
||||
_ = keysToCopyReader.CloseWithError(fmt.Errorf("git annex copy --batch-keys --from <remote> --to <remote> [%s]: %w - %s", repoPath, err, errbuf.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func ContentLocationFromKey(repoPath, key string) (string, error) {
|
||||
contentLocation, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "contentlocation").AddDynamicArguments(key).RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("in %s: %s does not seem to be a valid annexed file: %w", repoPath, key, err)
|
||||
}
|
||||
contentLocation = strings.TrimSpace(contentLocation)
|
||||
contentLocation = path.Clean("/" + contentLocation)[1:] // prevent directory traversals
|
||||
contentLocation = path.Join(repoPath, contentLocation)
|
||||
|
||||
return contentLocation, nil
|
||||
}
|
||||
|
||||
// return the absolute path of the content pointed to by the annex pointer stored in the git object
|
||||
// errors if the content is not found in this repo
|
||||
func ContentLocation(blob *git.Blob) (string, error) {
|
||||
key, err := LookupKey(blob)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ContentLocationFromKey(blob.Repo().Path, key)
|
||||
}
|
||||
|
||||
// returns a stream open to the annex content
|
||||
func Content(blob *git.Blob) (*os.File, error) {
|
||||
contentLocation, err := ContentLocation(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.Open(contentLocation)
|
||||
}
|
||||
|
||||
// whether the object appears to be a valid annex pointer
|
||||
// does *not* verify if the content is actually in this repo;
|
||||
// for that, use ContentLocation()
|
||||
func IsAnnexed(blob *git.Blob) (bool, error) {
|
||||
if !setting.Annex.Enabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// LookupKey is written to only return well-formed keys
|
||||
// so the test is just to see if it errors
|
||||
_, err := LookupKey(blob)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBlobIsNotAnnexed) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PathIsAnnexRepo determines if repoPath is a git-annex enabled repository
|
||||
func PathIsAnnexRepo(repoPath string) bool {
|
||||
_, _, err := git.NewCommand(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsAnnexRepo determines if repo is a git-annex enabled repository
|
||||
func IsAnnexRepo(repo *git.Repository) bool {
|
||||
_, _, err := git.NewCommand(repo.Ctx, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repo.Path})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
var (
|
||||
uuid2repoPathCache = make(map[string]string)
|
||||
repoPath2uuidCache = make(map[string]string)
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
if !setting.Annex.Enabled {
|
||||
return nil
|
||||
}
|
||||
if !setting.Annex.DisableP2PHTTP {
|
||||
log.Info("Populating the git-annex UUID cache with existing repositories")
|
||||
start := time.Now()
|
||||
if err := updateUUID2RepoPathCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Populating the git-annex UUID cache took %v", time.Since(start))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateUUID2RepoPathCache() error {
|
||||
configFiles, err := filepath.Glob(filepath.Join(setting.RepoRootPath, "*", "*", "config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, configFile := range configFiles {
|
||||
repoPath := strings.TrimSuffix(configFile, "/config")
|
||||
_, ok := repoPath2uuidCache[repoPath]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
config, err := ini.Load(configFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
repoUUID := config.Section("annex").Key("uuid").Value()
|
||||
if repoUUID != "" {
|
||||
uuid2repoPathCache[repoUUID] = repoPath
|
||||
repoPath2uuidCache[repoPath] = repoUUID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func repoPathFromUUIDCache(uuid string) (string, error) {
|
||||
if repoPath, ok := uuid2repoPathCache[uuid]; ok {
|
||||
return repoPath, nil
|
||||
}
|
||||
// If the cache didn't contain an entry for the UUID then update the cache and try again
|
||||
if err := updateUUID2RepoPathCache(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if repoPath, ok := uuid2repoPathCache[uuid]; ok {
|
||||
return repoPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("no repository known for UUID '%s'", uuid)
|
||||
}
|
||||
|
||||
func checkValidity(uuid, repoPath string) (bool, error) {
|
||||
stdout, _, err := git.NewCommand(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
repoUUID := strings.TrimSpace(stdout)
|
||||
return uuid == repoUUID, nil
|
||||
}
|
||||
|
||||
func removeCachedEntries(uuid, repoPath string) {
|
||||
delete(uuid2repoPathCache, uuid)
|
||||
delete(repoPath2uuidCache, repoPath)
|
||||
}
|
||||
|
||||
func UUID2RepoPath(uuid string) (string, error) {
|
||||
// Get the current cache entry for the UUID
|
||||
repoPath, err := repoPathFromUUIDCache(uuid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Check if it is still up-to-date
|
||||
valid, err := checkValidity(uuid, repoPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !valid {
|
||||
// If it isn't, remove the cache entry and try again
|
||||
removeCachedEntries(uuid, repoPath)
|
||||
return UUID2RepoPath(uuid)
|
||||
}
|
||||
// Otherwise just return the cached entry
|
||||
return repoPath, nil
|
||||
}
|
||||
|
||||
// GuessContentType guesses the content type of the annexed blob.
|
||||
func GuessContentType(blob *git.Blob) (typesniffer.SniffedType, error) {
|
||||
r, err := Content(blob)
|
||||
if err != nil {
|
||||
return typesniffer.SniffedType{}, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return typesniffer.DetectContentTypeFromReader(r)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -106,3 +106,36 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BlobsLessThanOrEqual32KiBFromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <=32KiB in size
|
||||
func BlobsLessThanOrEqual32KiBFromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
defer catFileCheckReader.Close()
|
||||
scanner := bufio.NewScanner(catFileCheckReader)
|
||||
defer func() {
|
||||
_ = shasToBatchWriter.CloseWithError(scanner.Err())
|
||||
}()
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(line, " ")
|
||||
if len(fields) < 3 || fields[1] != "blob" {
|
||||
continue
|
||||
}
|
||||
size, _ := strconv.Atoi(fields[2])
|
||||
if size > 32*1024 {
|
||||
continue
|
||||
}
|
||||
toWrite := []byte(fields[0] + "\n")
|
||||
for len(toWrite) > 0 {
|
||||
n, err := shasToBatchWriter.Write(toWrite)
|
||||
if err != nil {
|
||||
_ = catFileCheckReader.CloseWithError(err)
|
||||
break
|
||||
}
|
||||
toWrite = toWrite[n:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
modules/markup/external/external.go
vendored
25
modules/markup/external/external.go
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,6 +40,7 @@ type ServCommandResults struct {
|
|||
UserName string
|
||||
UserEmail string
|
||||
UserID int64
|
||||
UserMode perm.AccessMode
|
||||
OwnerName string
|
||||
RepoName string
|
||||
RepoID int64
|
||||
|
|
25
modules/setting/annex.go
Normal file
25
modules/setting/annex.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Annex represents the configuration for git-annex
|
||||
var Annex = struct {
|
||||
Enabled bool `ini:"ENABLED"`
|
||||
DisableP2PHTTP bool `ini:"DISABLE_P2PHTTP"`
|
||||
}{}
|
||||
|
||||
func loadAnnexFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("annex")
|
||||
if err := sec.MapTo(&Annex); err != nil {
|
||||
log.Fatal("Failed to map Annex settings: %v", err)
|
||||
}
|
||||
if !sec.HasKey("DISABLE_P2PHTTP") {
|
||||
// If DisableP2PHTTP is not explicitly set then use DisableHTTPGit as its default
|
||||
Annex.DisableP2PHTTP = Repository.DisableHTTPGit
|
||||
}
|
||||
}
|
|
@ -153,6 +153,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||
loadCamoFrom(cfg)
|
||||
loadI18nFrom(cfg)
|
||||
loadGitFrom(cfg)
|
||||
loadAnnexFrom(cfg)
|
||||
loadMirrorFrom(cfg)
|
||||
loadMarkupFrom(cfg)
|
||||
loadQuotaFrom(cfg)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1317,6 +1317,7 @@ view_git_blame=Zobrazit git blame
|
|||
video_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „video“.
|
||||
audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „audio“.
|
||||
stored_lfs=Uloženo pomocí Git LFS
|
||||
stored_annex=Uloženo pomocí Git Annex
|
||||
symbolic_link=Symbolický odkaz
|
||||
executable_file=Spustitelný soubor
|
||||
vendored = Vendorováno
|
||||
|
@ -1342,6 +1343,7 @@ editor.upload_file=Nahrát soubor
|
|||
editor.edit_file=Upravit soubor
|
||||
editor.preview_changes=Náhled změn
|
||||
editor.cannot_edit_lfs_files=LFS soubory nemohou být upravovány přes webové rozhraní.
|
||||
editor.cannot_edit_annex_files=Annex soubory nemohou být upravovány přes webové rozhraní.
|
||||
editor.cannot_edit_non_text_files=Binární soubory nemohou být upravovány přes webové rozhraní.
|
||||
editor.edit_this_file=Upravit soubor
|
||||
editor.this_file_locked=Soubor je uzamčen
|
||||
|
|
|
@ -1317,6 +1317,8 @@ view_git_blame=„git blame“ ansehen
|
|||
video_not_supported_in_browser=Dein Browser unterstützt das HTML5-„video“-Tag nicht.
|
||||
audio_not_supported_in_browser=Dein Browser unterstützt das HTML5-„audio“-Tag nicht.
|
||||
stored_lfs=Gespeichert mit Git LFS
|
||||
stored_annex=Gespeichert mit Git Annex
|
||||
stored_annex_not_present = hier nicht vorhanden, versuche git annex whereis
|
||||
symbolic_link=Softlink
|
||||
executable_file=Ausführbare Datei
|
||||
commit_graph=Commit-Graph
|
||||
|
@ -1340,6 +1342,7 @@ editor.upload_file=Datei hochladen
|
|||
editor.edit_file=Datei bearbeiten
|
||||
editor.preview_changes=Vorschau der Änderungen
|
||||
editor.cannot_edit_lfs_files=LFS-Dateien können im Webinterface nicht bearbeitet werden.
|
||||
editor.cannot_edit_annex_files=Annex-Dateien können im Webinterface nicht bearbeitet werden.
|
||||
editor.cannot_edit_non_text_files=Binärdateien können nicht im Webinterface bearbeitet werden.
|
||||
editor.edit_this_file=Datei bearbeiten
|
||||
editor.this_file_locked=Datei ist gesperrt
|
||||
|
|
|
@ -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=Το αρχείο είναι κλειδωμένο
|
||||
|
|
|
@ -1337,6 +1337,8 @@ view_git_blame = View git blame
|
|||
video_not_supported_in_browser = Your browser does not support the HTML5 "video" tag.
|
||||
audio_not_supported_in_browser = Your browser does not support the HTML5 "audio" tag.
|
||||
stored_lfs = Stored with Git LFS
|
||||
stored_annex = Stored with Git Annex
|
||||
stored_annex_not_present = not present here, try using git annex whereis
|
||||
symbolic_link = Symbolic link
|
||||
executable_file = Executable file
|
||||
vendored = Vendored
|
||||
|
@ -1364,6 +1366,7 @@ editor.upload_file = Upload file
|
|||
editor.edit_file = Edit file
|
||||
editor.preview_changes = Preview changes
|
||||
editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface.
|
||||
editor.cannot_edit_annex_files = Annex files cannot be edited in the web interface.
|
||||
editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface.
|
||||
editor.edit_this_file = Edit file
|
||||
editor.this_file_locked = File is locked
|
||||
|
|
|
@ -1312,6 +1312,7 @@ view_git_blame=Ver Git blame
|
|||
video_not_supported_in_browser=Su navegador no soporta el tag "video" de HTML5.
|
||||
audio_not_supported_in_browser=Su navegador no soporta el tag "audio" de HTML5.
|
||||
stored_lfs=Almacenados con Git LFS
|
||||
stored_annex=Almacenados con Git Annex
|
||||
symbolic_link=Enlace simbólico
|
||||
executable_file=Archivo ejecutable
|
||||
commit_graph=Gráfico de commits
|
||||
|
@ -1335,6 +1336,7 @@ editor.upload_file=Subir archivo
|
|||
editor.edit_file=Editar archivo
|
||||
editor.preview_changes=Vista previa de los cambios
|
||||
editor.cannot_edit_lfs_files=Los archivos LFS no se pueden editar en la interfaz web.
|
||||
editor.cannot_edit_annex_files=Los archivos Annex no se pueden editar en la interfaz web.
|
||||
editor.cannot_edit_non_text_files=Los archivos binarios no se pueden editar en la interfaz web.
|
||||
editor.edit_this_file=Editar archivo
|
||||
editor.this_file_locked=El archivo está bloqueado
|
||||
|
|
|
@ -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=پرونده قفل شده است
|
||||
|
|
|
@ -1318,6 +1318,7 @@ view_git_blame=Voir Git blame
|
|||
video_not_supported_in_browser=Votre navigateur ne supporte pas la balise « vidéo » HTML5.
|
||||
audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « audio » HTML5.
|
||||
stored_lfs=Stocké avec Git LFS
|
||||
stored_annex=Stocké avec Git Annex
|
||||
symbolic_link=Lien symbolique
|
||||
executable_file=Fichier exécutable
|
||||
vendored = Vendored
|
||||
|
@ -1343,6 +1344,7 @@ editor.upload_file=Téléverser un fichier
|
|||
editor.edit_file=Modifier le fichier
|
||||
editor.preview_changes=Aperçu des modifications
|
||||
editor.cannot_edit_lfs_files=Les fichiers LFS ne peuvent pas être modifiés dans l'interface web.
|
||||
editor.cannot_edit_annex_files=Les fichiers Annex ne peuvent pas être modifiés dans l'interface web.
|
||||
editor.cannot_edit_non_text_files=Les fichiers binaires ne peuvent pas être édités dans l'interface web.
|
||||
editor.edit_this_file=Modifier le fichier
|
||||
editor.this_file_locked=Le fichier est verrouillé
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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ð
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=ファイルはロックされています
|
||||
|
|
|
@ -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=새 파일
|
||||
|
|
|
@ -1316,6 +1316,7 @@ view_git_blame=Apskatīt Git izmaiņu veicējus
|
|||
video_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "video".
|
||||
audio_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "audio".
|
||||
stored_lfs=Saglabāts Git LFS
|
||||
stored_annex=Saglabāts Git Annex
|
||||
symbolic_link=Simboliska saite
|
||||
executable_file=Izpildāma datne
|
||||
commit_graph=Iesūtījumu karte
|
||||
|
@ -1339,6 +1340,7 @@ editor.upload_file=Augšupielādēt datni
|
|||
editor.edit_file=Labot datni
|
||||
editor.preview_changes=Priekšskatīt izmaiņas
|
||||
editor.cannot_edit_lfs_files=LFS datnes tīmekļa saskarnē nevar labot.
|
||||
editor.cannot_edit_annex_files=Annex datnes tīmekļa saskarnē nevar labot.
|
||||
editor.cannot_edit_non_text_files=Binārās datnes tīmekļa saskarnē nevar labot.
|
||||
editor.edit_this_file=Labot datni
|
||||
editor.this_file_locked=Datne ir slēgta
|
||||
|
@ -4027,4 +4029,4 @@ filepreview.lines = %[1]d. līdz %[2]d. rinda %[3]s
|
|||
filepreview.truncated = Priekšskatījums tika saīsināts
|
||||
|
||||
[translation_meta]
|
||||
test = Šī ir pārbaudes virkne. Tā netiek attēlota Forgejo saskarnē, bet tiek izmantota pārbaudes nolūkiem. Droši var ievadīt "ok", lai ietaupītu laiku (vai kādu jautru faktu pēc izvēles), lai sasniegtu to saldo 100% pabeigšanas atzīmi.
|
||||
test = Šī ir pārbaudes virkne. Tā netiek attēlota Forgejo saskarnē, bet tiek izmantota pārbaudes nolūkiem. Droši var ievadīt "ok", lai ietaupītu laiku (vai kādu jautru faktu pēc izvēles), lai sasniegtu to saldo 100% pabeigšanas atzīmi.
|
||||
|
|
|
@ -1285,6 +1285,7 @@ view_git_blame=Bekijk git blame
|
|||
video_not_supported_in_browser=Uw browser ondersteunt de HTML5 "video" element niet.
|
||||
audio_not_supported_in_browser=Uw browser ondersteunt de HTML5 "audio" element niet.
|
||||
stored_lfs=Opgeslagen met Git LFS
|
||||
stored_annex=Opgeslagen met Git Annex
|
||||
symbolic_link=Symbolische link
|
||||
commit_graph=Commit grafiek
|
||||
commit_graph.select=Selecteer branches
|
||||
|
@ -1303,6 +1304,7 @@ editor.upload_file=Upload bestand
|
|||
editor.edit_file=Bewerk bestand
|
||||
editor.preview_changes=Voorbeeld tonen
|
||||
editor.cannot_edit_lfs_files=LFS-bestanden kunnen niet worden bewerkt in de webinterface.
|
||||
editor.cannot_edit_annex_files=Annex-bestanden kunnen niet worden bewerkt in de webinterface.
|
||||
editor.cannot_edit_non_text_files=Binaire bestanden kunnen niet worden bewerkt in de webinterface.
|
||||
editor.edit_this_file=Bewerk bestand
|
||||
editor.this_file_locked=Bestand is vergrendeld
|
||||
|
|
|
@ -1249,6 +1249,7 @@ file_copy_permalink=Kopiuj bezpośredni odnośnik
|
|||
video_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "video".
|
||||
audio_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "audio".
|
||||
stored_lfs=Przechowane za pomocą Git LFS
|
||||
stored_annex=Przechowane za pomocą Git Annex
|
||||
symbolic_link=Dowiązanie symboliczne
|
||||
commit_graph=Wykres commitów
|
||||
commit_graph.select=Wybierz gałęzie
|
||||
|
@ -1266,6 +1267,7 @@ editor.upload_file=Wyślij plik
|
|||
editor.edit_file=Edytuj plik
|
||||
editor.preview_changes=Podgląd zmian
|
||||
editor.cannot_edit_lfs_files=Pliki LFS nie mogą być edytowane poprzez interfejs przeglądarkowy.
|
||||
editor.cannot_edit_annex_files=Pliki Annex nie mogą być edytowane poprzez interfejs przeglądarkowy.
|
||||
editor.cannot_edit_non_text_files=Pliki binarne nie mogą być edytowane poprzez interfejs przeglądarkowy.
|
||||
editor.edit_this_file=Edytuj plik
|
||||
editor.this_file_locked=Plik jest zablokowany
|
||||
|
|
|
@ -1310,6 +1310,7 @@ view_git_blame=Ver git blame
|
|||
video_not_supported_in_browser=Seu navegador não tem suporte para a tag "video" do HTML5.
|
||||
audio_not_supported_in_browser=Seu navegador não tem suporte para a tag "audio" do HTML5.
|
||||
stored_lfs=Armazenado com Git LFS
|
||||
stored_annex=Armazenado com Git Annex
|
||||
symbolic_link=Link simbólico
|
||||
executable_file=Arquivo executável
|
||||
commit_graph=Gráfico de commits
|
||||
|
@ -1333,6 +1334,7 @@ editor.upload_file=Enviar arquivo
|
|||
editor.edit_file=Editar arquivo
|
||||
editor.preview_changes=Pré-visualizar alterações
|
||||
editor.cannot_edit_lfs_files=Arquivos LFS não podem ser editados na interface web.
|
||||
editor.cannot_edit_annex_files=Arquivos Annex não podem ser editados na interface web.
|
||||
editor.cannot_edit_non_text_files=Arquivos binários não podem ser editados na interface web.
|
||||
editor.edit_this_file=Editar arquivo
|
||||
editor.this_file_locked=Arquivo está bloqueado
|
||||
|
|
|
@ -1321,6 +1321,7 @@ view_git_blame=Ver git blame
|
|||
video_not_supported_in_browser=O seu navegador não suporta a etiqueta "video" do HTML5.
|
||||
audio_not_supported_in_browser=O seu navegador não suporta a etiqueta "audio" do HTML5.
|
||||
stored_lfs=Armazenado com Git LFS
|
||||
stored_annex=Armazenado com Git Annex
|
||||
symbolic_link=Ligação simbólica
|
||||
executable_file=Ficheiro executável
|
||||
vendored=Externo
|
||||
|
@ -1346,6 +1347,7 @@ editor.upload_file=Carregar ficheiro
|
|||
editor.edit_file=Editar ficheiro
|
||||
editor.preview_changes=Pré-visualizar modificações
|
||||
editor.cannot_edit_lfs_files=Ficheiros LFS não podem ser editados na interface web.
|
||||
editor.cannot_edit_annex_files=Ficheiros Annex não podem ser editados na interface web.
|
||||
editor.cannot_edit_non_text_files=Ficheiros binários não podem ser editados na interface da web.
|
||||
editor.edit_this_file=Editar ficheiro
|
||||
editor.this_file_locked=Ficheiro bloqueado
|
||||
|
|
|
@ -1304,6 +1304,7 @@ view_git_blame=Показать git blame
|
|||
video_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «video».
|
||||
audio_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «audio».
|
||||
stored_lfs=Хранится Git LFS
|
||||
stored_annex=Хранится Git Annex
|
||||
symbolic_link=Символическая ссылка
|
||||
executable_file=Исполняемый файл
|
||||
commit_graph=Граф коммитов
|
||||
|
@ -1327,6 +1328,7 @@ editor.upload_file=Загрузить файл
|
|||
editor.edit_file=Редактировать файл
|
||||
editor.preview_changes=Просмотр изменений
|
||||
editor.cannot_edit_lfs_files=LFS файлы невозможно редактировать в веб-интерфейсе.
|
||||
editor.cannot_edit_annex_files=Annex файлы невозможно редактировать в веб-интерфейсе.
|
||||
editor.cannot_edit_non_text_files=Двоичные файлы нельзя редактировать в веб-интерфейсе.
|
||||
editor.edit_this_file=Редактировать файл
|
||||
editor.this_file_locked=Файл заблокирован
|
||||
|
|
|
@ -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=ගොනුවට අගුළු ලා ඇත
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -910,6 +910,7 @@ file_too_large=Filen är för stor för att visas.
|
|||
video_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen "video".
|
||||
audio_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen "audio".
|
||||
stored_lfs=Sparad med Git LFS
|
||||
stored_annex=Sparad med Git Annex
|
||||
symbolic_link=Symbolisk länk
|
||||
commit_graph=Commitgraf
|
||||
commit_graph.monochrome=Mono
|
||||
|
@ -923,6 +924,7 @@ editor.upload_file=Ladda upp fil
|
|||
editor.edit_file=Redigera fil
|
||||
editor.preview_changes=Förhandsgranska ändringar
|
||||
editor.cannot_edit_lfs_files=LFS-filer kan inte redigeras i webbgränssnittet.
|
||||
editor.cannot_edit_annex_files=Annex-filer kan inte redigeras i webbgränssnittet.
|
||||
editor.cannot_edit_non_text_files=Binära filer kan inte redigeras genom webbgränssnittet.
|
||||
editor.edit_this_file=Redigera fil
|
||||
editor.this_file_locked=Filen är låst
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1243,6 +1243,7 @@ file_copy_permalink=Копіювати постійне посилання
|
|||
video_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «video».
|
||||
audio_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «audio».
|
||||
stored_lfs=Збережено з Git LFS
|
||||
stored_annex=Збережено з Git Annex
|
||||
symbolic_link=Символічне посилання
|
||||
commit_graph=Графік комітів
|
||||
commit_graph.select=Виберіть гілки
|
||||
|
@ -1260,6 +1261,7 @@ editor.upload_file=Завантажити файл
|
|||
editor.edit_file=Редагувати файл
|
||||
editor.preview_changes=Попередній перегляд змін
|
||||
editor.cannot_edit_lfs_files=Файли LFS не можна редагувати в веб-інтерфейсі.
|
||||
editor.cannot_edit_annex_files=Файли Annex не можна редагувати в веб-інтерфейсі.
|
||||
editor.cannot_edit_non_text_files=Бінарні файли не можливо редагувати у веб-інтерфейсі.
|
||||
editor.edit_this_file=Редагувати файл
|
||||
editor.this_file_locked=Файл заблоковано
|
||||
|
|
|
@ -1319,6 +1319,7 @@ view_git_blame=查看 Git Blame
|
|||
video_not_supported_in_browser=您的浏览器不支持 HTML5 “video” 标签。
|
||||
audio_not_supported_in_browser=您的浏览器不支持 HTML5 “audio” 标签。
|
||||
stored_lfs=存储到Git LFS
|
||||
stored_annex=存储到Git Annex
|
||||
symbolic_link=符号链接
|
||||
executable_file=可执行文件
|
||||
vendored = Vendored
|
||||
|
@ -1344,6 +1345,7 @@ editor.upload_file=上传文件
|
|||
editor.edit_file=编辑文件
|
||||
editor.preview_changes=预览变更
|
||||
editor.cannot_edit_lfs_files=无法在 web 界面中编辑 lfs 文件。
|
||||
editor.cannot_edit_annex_files=无法在 web 界面中编辑 lfs 文件。
|
||||
editor.cannot_edit_non_text_files=网页不能编辑二进制文件。
|
||||
editor.edit_this_file=编辑文件
|
||||
editor.this_file_locked=文件已锁定
|
||||
|
|
|
@ -472,6 +472,7 @@ file_view_raw=查看原始文件
|
|||
file_permalink=永久連結
|
||||
|
||||
stored_lfs=儲存到到 Git LFS
|
||||
stored_annex=儲存到到 Git Annex
|
||||
|
||||
editor.preview_changes=預覽更改
|
||||
editor.or=或
|
||||
|
@ -1132,4 +1133,4 @@ runners.labels = 標籤
|
|||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
[git.filemode]
|
||||
|
|
|
@ -1293,6 +1293,7 @@ view_git_blame=檢視 Git Blame
|
|||
video_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「video」標籤。
|
||||
audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「audio」標籤。
|
||||
stored_lfs=已使用 Git LFS 儲存
|
||||
stored_annex=已使用 Git Annex 儲存
|
||||
symbolic_link=符號連結
|
||||
commit_graph=提交線圖
|
||||
commit_graph.select=選擇分支
|
||||
|
@ -1312,6 +1313,7 @@ editor.upload_file=上傳檔案
|
|||
editor.edit_file=編輯檔案
|
||||
editor.preview_changes=預覽變更
|
||||
editor.cannot_edit_lfs_files=無法在 web 介面中編輯 LFS 檔。
|
||||
editor.cannot_edit_annex_files=無法在 web 介面中編輯 Annex 檔。
|
||||
editor.cannot_edit_non_text_files=網站介面不能編輯二進位檔案。
|
||||
editor.edit_this_file=編輯檔案
|
||||
editor.this_file_locked=檔案已被鎖定
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "forgejo",
|
||||
"name": "forgejo-aneksajo",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -81,12 +81,14 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
ownerName := ctx.Params(":owner")
|
||||
repoName := ctx.Params(":repo")
|
||||
mode := perm.AccessMode(ctx.FormInt("mode"))
|
||||
verbs := ctx.FormStrings("verb")
|
||||
|
||||
// Set the basic parts of the results to return
|
||||
results := private.ServCommandResults{
|
||||
RepoName: repoName,
|
||||
OwnerName: ownerName,
|
||||
KeyID: keyID,
|
||||
UserMode: perm.AccessModeNone,
|
||||
}
|
||||
|
||||
// Now because we're not translating things properly let's just default some English strings here
|
||||
|
@ -287,8 +289,10 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
repo.IsPrivate ||
|
||||
owner.Visibility.IsPrivate() ||
|
||||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
|
||||
(setting.Annex.Enabled && len(verbs) > 0 && verbs[0] == "git-annex-shell") || // git-annex has its own permission enforcement, for which we expose results.UserMode
|
||||
setting.Service.RequireSignInView) {
|
||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||
results.UserMode = deployKey.Mode
|
||||
if deployKey.Mode < mode {
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
|
||||
|
@ -310,9 +314,9 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
return
|
||||
}
|
||||
|
||||
userMode := perm.UnitAccessMode(unitType)
|
||||
results.UserMode = perm.UnitAccessMode(unitType)
|
||||
|
||||
if userMode < mode {
|
||||
if results.UserMode < mode {
|
||||
log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr())
|
||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
||||
UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName),
|
||||
|
@ -353,6 +357,7 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
})
|
||||
return
|
||||
}
|
||||
results.UserMode = perm.AccessModeWrite
|
||||
results.RepoID = repo.ID
|
||||
}
|
||||
|
||||
|
@ -381,13 +386,14 @@ func ServCommand(ctx *context.PrivateContext) {
|
|||
return
|
||||
}
|
||||
}
|
||||
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
|
||||
log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nUserMode: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d",
|
||||
results.IsWiki,
|
||||
results.DeployKeyID,
|
||||
results.KeyID,
|
||||
results.KeyName,
|
||||
results.UserName,
|
||||
results.UserID,
|
||||
results.UserMode,
|
||||
results.OwnerName,
|
||||
results.RepoName,
|
||||
results.RepoID)
|
||||
|
|
146
routers/web/repo/annex.go
Normal file
146
routers/web/repo/annex.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/annex"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
services_context "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
type p2phttpRecordType struct {
|
||||
CancelFunc func()
|
||||
LastUsed time.Time
|
||||
Port string
|
||||
}
|
||||
|
||||
var p2phttpRecords = make(map[string]*p2phttpRecordType)
|
||||
|
||||
// AnnexP2PHTTP implements git-annex smart HTTP support by delegating to git annex p2phttp
|
||||
func AnnexP2PHTTP(ctx *services_context.Context) {
|
||||
uuid := ctx.Params(":uuid")
|
||||
repoPath, err := annex.UUID2RepoPath(uuid)
|
||||
if err != nil {
|
||||
ctx.PlainText(http.StatusNotFound, "Repository not found")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(repoPath, "/")
|
||||
repoName := strings.TrimSuffix(parts[len(parts)-1], ".git")
|
||||
owner := parts[len(parts)-2]
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
|
||||
if err != nil {
|
||||
ctx.PlainText(http.StatusNotFound, "Repository not found")
|
||||
return
|
||||
}
|
||||
|
||||
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !(ctx.Req.Method == "GET" && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
|
||||
ctx.Req.Method == "POST" && p.CanAccess(perm.AccessModeWrite, unit.TypeCode) ||
|
||||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/checkpresent") && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
|
||||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/keeplocked") ||
|
||||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/lockcontent")) {
|
||||
// GET requests require at least read access; POST requests for
|
||||
// anything but checkpresent, lockcontent, and keeplocked
|
||||
// require write permissions; POST requests for checkpresent
|
||||
// only require read permissions, as it really is just a read.
|
||||
// POST requests for lockcontent and keeplocked require no
|
||||
// authentication at all, as is also the case for the
|
||||
// authentication in the git-annex-p2phttp server. See
|
||||
// https://git-annex.branchable.com/bugs/p2phttp__58___drop_difference_wideopen_unauth-readonly/
|
||||
// for reasoning.
|
||||
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
p2phttpRecord, p2phttpProcessExists := p2phttpRecords[uuid]
|
||||
if p2phttpProcessExists {
|
||||
p2phttpRecord.LastUsed = time.Now()
|
||||
} else {
|
||||
// Start a new p2phttp process for the requested repository
|
||||
// There is a race condition here with the port selection, ideally git annex p2phttp could just listen on a unix socket...
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
log.Error("Failed to listen on a free port: %v", err)
|
||||
ctx.Resp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
hopefullyFreePort := strings.SplitN(lis.Addr().String(), ":", 2)[1]
|
||||
lis.Close()
|
||||
p2phttpCtx, p2phttpCtxCancel := context.WithCancel(context.Background())
|
||||
go func(ctx context.Context) {
|
||||
cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "annex", "p2phttp", "-J2", "--bind", "127.0.0.1", "--wideopen", "--port", hopefullyFreePort)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Pdeathsig: syscall.SIGINT,
|
||||
}
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }
|
||||
_ = cmd.Run()
|
||||
}(p2phttpCtx)
|
||||
graceful.GetManager().RunAtTerminate(p2phttpCtxCancel)
|
||||
|
||||
// Wait for the p2phttp server to get ready
|
||||
start := time.Now()
|
||||
sleepDuration := 1 * time.Millisecond
|
||||
for {
|
||||
if time.Since(start) > 5*time.Second {
|
||||
p2phttpCtxCancel()
|
||||
log.Error("Failed to start the p2phttp server in a reasonable amount of time")
|
||||
ctx.Resp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+hopefullyFreePort)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
break
|
||||
}
|
||||
time.Sleep(sleepDuration)
|
||||
sleepDuration *= 2
|
||||
if sleepDuration > 1*time.Second {
|
||||
sleepDuration = 1 * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
p2phttpRecord = &p2phttpRecordType{CancelFunc: p2phttpCtxCancel, LastUsed: time.Now(), Port: hopefullyFreePort}
|
||||
p2phttpRecords[uuid] = p2phttpRecord
|
||||
}
|
||||
|
||||
// Cleanup p2phttp processes that haven't been used for a while
|
||||
for uuid, record := range p2phttpRecords {
|
||||
if time.Since(record.LastUsed) > 5*time.Minute {
|
||||
record.CancelFunc()
|
||||
delete(p2phttpRecords, uuid)
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse("http://127.0.0.1:" + p2phttpRecord.Port + strings.TrimPrefix(ctx.Req.RequestURI, "/git-annex-p2phttp"))
|
||||
if err != nil {
|
||||
log.Error("Failed to parse URL: %v", err)
|
||||
ctx.Resp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
proxy := httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.Out.URL = url
|
||||
},
|
||||
}
|
||||
proxy.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -43,8 +43,8 @@ func (b *Basic) Name() string {
|
|||
// name/token on successful validation.
|
||||
// Returns nil if header is empty or validation fails.
|
||||
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||
// Basic authentication should only fire on API, Download or on Git, LFSPaths or Git-Annex paths
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) && !isAnnexPath(req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
61
services/pull/annex.go
Normal file
61
services/pull/annex.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/annex"
|
||||
"code.gitea.io/gitea/modules/git/pipeline"
|
||||
)
|
||||
|
||||
// AnnexPush copies all annexed files referenced in new commits from the head repository to the base repository
|
||||
func AnnexPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string) error {
|
||||
// Initialize the temporary repository with git-annex
|
||||
if err := annex.PrivateInit(ctx, tmpBasePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
revListReader, revListWriter := io.Pipe()
|
||||
shasToCheckReader, shasToCheckWriter := io.Pipe()
|
||||
catFileCheckReader, catFileCheckWriter := io.Pipe()
|
||||
shasToBatchReader, shasToBatchWriter := io.Pipe()
|
||||
lookupKeyBatchReader, lookupKeyBatchWriter := io.Pipe()
|
||||
errChan := make(chan error, 1)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(6)
|
||||
// Create the go-routines in reverse order.
|
||||
|
||||
// 6. Take the referenced keys and copy their data from the head repository to
|
||||
// the base repository
|
||||
go annex.CopyFromToBatch(ctx, "head_repo", "origin", lookupKeyBatchReader, &wg, tmpBasePath)
|
||||
|
||||
// 5. Take the shas of the blobs and resolve them to annex keys, git-annex
|
||||
// should filter out anything that doesn't reference a key
|
||||
go annex.LookupKeyBatch(ctx, shasToBatchReader, lookupKeyBatchWriter, &wg, tmpBasePath)
|
||||
|
||||
// 4. From the provided objects restrict to blobs <=32KiB
|
||||
go pipeline.BlobsLessThanOrEqual32KiBFromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
|
||||
|
||||
// 3. Run batch-check on the objects retrieved from rev-list
|
||||
go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, tmpBasePath)
|
||||
|
||||
// 2. Check each object retrieved rejecting those without names as they will be commits or trees
|
||||
go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
|
||||
|
||||
// 1. Run rev-list objects from mergeHead to mergeBase
|
||||
go pipeline.RevListObjects(ctx, revListWriter, &wg, tmpBasePath, mergeHeadSHA, mergeBaseSHA, errChan)
|
||||
|
||||
wg.Wait()
|
||||
select {
|
||||
case err, has := <-errChan:
|
||||
if has {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -22,6 +22,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/annex"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -314,6 +315,12 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
|
|||
}
|
||||
}
|
||||
|
||||
if setting.Annex.Enabled && annex.PathIsAnnexRepo(pr.BaseRepo.RepoPath()) && annex.PathIsAnnexRepo(pr.HeadRepo.RepoPath()) {
|
||||
if err := AnnexPush(ctx, mergeCtx.tmpBasePath, mergeHeadSHA, mergeBaseSHA); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var headUser *user_model.User
|
||||
err = pr.HeadRepo.LoadOwner(ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
{{if .FileSize}}
|
||||
<div class="file-info-entry">
|
||||
{{ctx.Locale.TrSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
|
||||
{{if .IsAnnexFile}} ({{ctx.Locale.Tr "repo.stored_annex"}}{{if not .IsAnnexFilePresent}} - {{ctx.Locale.Tr "repo.stored_annex_not_present"}}{{end}}){{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .LFSLock}}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
2983
tests/integration/git_annex_test.go
Normal file
2983
tests/integration/git_annex_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -122,6 +122,9 @@ MINIO_LOCATION = us-east-1
|
|||
MINIO_USE_SSL = false
|
||||
MINIO_CHECKSUM_ALGORITHM = md5
|
||||
|
||||
[annex]
|
||||
ENABLED = true
|
||||
|
||||
[packages]
|
||||
ENABLED = true
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue