mirror of
https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo.git
synced 2025-07-23 23:00:06 +02:00
Compare commits
47 commits
941978ccd6
...
7bbc84ed5e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7bbc84ed5e | ||
![]() |
19be11668b | ||
![]() |
8e19674840 | ||
![]() |
a4dd1bcc4b | ||
![]() |
aba710a962 | ||
![]() |
a0ca7bfb6b | ||
![]() |
5bcf4c1821 | ||
![]() |
ebac406127 | ||
![]() |
1dd008f8e1 | ||
![]() |
af3febc693 | ||
![]() |
c659e4befa | ||
![]() |
5b3d77e3b2 | ||
![]() |
14c0633698 | ||
![]() |
eba1a96263 | ||
![]() |
59510cd90d | ||
![]() |
be55d4233a | ||
![]() |
2aef6a880d | ||
![]() |
b41c692e04 | ||
![]() |
71338f3864 | ||
![]() |
8b50a576ec | ||
![]() |
0dc35349e4 | ||
![]() |
688f53d752 | ||
![]() |
88095d85ea | ||
![]() |
4e03aa9095 | ||
![]() |
ad8476c821 | ||
![]() |
aef3b8b31f | ||
![]() |
3b7db37075 | ||
![]() |
276114f289 | ||
![]() |
3a689a3514 | ||
![]() |
f33d0976c6 | ||
![]() |
98e5f66122 | ||
![]() |
65ed342bf9 | ||
![]() |
75afe98f5c | ||
![]() |
5288b230d2 | ||
![]() |
e4c5a64499 | ||
![]() |
bb96dc35e9 | ||
![]() |
3d9d84ad70 | ||
![]() |
90d53d48a5 | ||
![]() |
9482ff6dd1 | ||
![]() |
ed712c00ba | ||
![]() |
aa1b739ead | ||
![]() |
ee7f8249d1 | ||
![]() |
c821e35ce3 | ||
![]() |
1d53f9f2cd | ||
![]() |
d998752903 | ||
![]() |
16c0285ca4 | ||
![]() |
8d3dd3aa02 |
75 changed files with 4362 additions and 117 deletions
|
@ -13,6 +13,8 @@ runs:
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
echo "deb http://deb.debian.org/debian/ ${RELEASE} main" > "/etc/apt/sources.list.d/${RELEASE}.list"
|
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:
|
env:
|
||||||
RELEASE: ${{inputs.release}}
|
RELEASE: ${{inputs.release}}
|
||||||
- name: install packages
|
- name: install packages
|
||||||
|
@ -24,6 +26,7 @@ runs:
|
||||||
- name: remove temporary package list to prevent using it in other steps
|
- name: remove temporary package list to prevent using it in other steps
|
||||||
run: |
|
run: |
|
||||||
rm "/etc/apt/sources.list.d/${RELEASE}.list"
|
rm "/etc/apt/sources.list.d/${RELEASE}.list"
|
||||||
|
rm "/etc/apt/sources.list.d/neurodebian.sources.list"
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
env:
|
env:
|
||||||
RELEASE: ${{inputs.release}}
|
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:
|
jobs:
|
||||||
backend-checks:
|
backend-checks:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||||
|
@ -27,7 +26,6 @@ jobs:
|
||||||
- run: su forgejo -c 'make --always-make -j$(nproc) lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate' # ensure the "go-licenses" make target runs
|
- run: su forgejo -c 'make --always-make -j$(nproc) lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate' # ensure the "go-licenses" make target runs
|
||||||
- uses: ./.forgejo/workflows-composite/build-backend
|
- uses: ./.forgejo/workflows-composite/build-backend
|
||||||
frontend-checks:
|
frontend-checks:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||||
|
@ -176,7 +174,6 @@ jobs:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
TEST_REDIS_SERVER: cacher:${{ matrix.cacher.port }}
|
TEST_REDIS_SERVER: cacher:${{ matrix.cacher.port }}
|
||||||
test-mysql:
|
test-mysql:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [backend-checks, frontend-checks]
|
needs: [backend-checks, frontend-checks]
|
||||||
container:
|
container:
|
||||||
|
@ -199,15 +196,13 @@ jobs:
|
||||||
- name: install dependencies & git >= 2.42
|
- name: install dependencies & git >= 2.42
|
||||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||||
with:
|
with:
|
||||||
packages: git git-lfs
|
packages: git git-annex-standalone git-lfs
|
||||||
- uses: ./.forgejo/workflows-composite/build-backend
|
- uses: ./.forgejo/workflows-composite/build-backend
|
||||||
- run: |
|
- run: |
|
||||||
su forgejo -c 'make test-mysql-migration test-mysql'
|
su forgejo -c 'make test-mysql-migration test-mysql'
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
env:
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
test-pgsql:
|
test-pgsql:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [backend-checks, frontend-checks]
|
needs: [backend-checks, frontend-checks]
|
||||||
container:
|
container:
|
||||||
|
@ -236,17 +231,15 @@ jobs:
|
||||||
- name: install dependencies & git >= 2.42
|
- name: install dependencies & git >= 2.42
|
||||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||||
with:
|
with:
|
||||||
packages: git git-lfs
|
packages: git git-annex-standalone git-lfs
|
||||||
- uses: ./.forgejo/workflows-composite/build-backend
|
- uses: ./.forgejo/workflows-composite/build-backend
|
||||||
- run: |
|
- run: |
|
||||||
su forgejo -c 'make test-pgsql-migration test-pgsql'
|
su forgejo -c 'make test-pgsql-migration test-pgsql'
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
env:
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
TEST_LDAP: 1
|
TEST_LDAP: 1
|
||||||
test-sqlite:
|
test-sqlite:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs: [backend-checks, frontend-checks]
|
needs: [backend-checks, frontend-checks]
|
||||||
container:
|
container:
|
||||||
|
@ -258,25 +251,21 @@ jobs:
|
||||||
- name: install dependencies & git >= 2.42
|
- name: install dependencies & git >= 2.42
|
||||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||||
with:
|
with:
|
||||||
packages: git git-lfs
|
packages: git git-annex-standalone git-lfs
|
||||||
- uses: ./.forgejo/workflows-composite/build-backend
|
- uses: ./.forgejo/workflows-composite/build-backend
|
||||||
- run: |
|
- run: |
|
||||||
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
||||||
timeout-minutes: 120
|
|
||||||
env:
|
env:
|
||||||
TAGS: sqlite sqlite_unlock_notify
|
TAGS: sqlite sqlite_unlock_notify
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
TEST_TAGS: sqlite sqlite_unlock_notify
|
TEST_TAGS: sqlite sqlite_unlock_notify
|
||||||
USE_REPO_TEST_DIR: 1
|
USE_REPO_TEST_DIR: 1
|
||||||
security-check:
|
security-check:
|
||||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
needs:
|
needs:
|
||||||
- test-sqlite
|
- test-sqlite
|
||||||
- test-pgsql
|
- test-pgsql
|
||||||
- test-mysql
|
- test-mysql
|
||||||
- test-remote-cacher
|
|
||||||
- test-unit
|
|
||||||
container:
|
container:
|
||||||
image: 'data.forgejo.org/oci/node:22-bookworm'
|
image: 'data.forgejo.org/oci/node:22-bookworm'
|
||||||
options: --tmpfs /tmp:exec,noatime
|
options: --tmpfs /tmp:exec,noatime
|
||||||
|
|
|
@ -78,6 +78,7 @@ RUN apk --no-cache add \
|
||||||
sqlite \
|
sqlite \
|
||||||
su-exec \
|
su-exec \
|
||||||
gnupg \
|
gnupg \
|
||||||
|
git-annex \
|
||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
RUN addgroup \
|
RUN addgroup \
|
||||||
|
|
|
@ -73,6 +73,7 @@ RUN apk --no-cache add \
|
||||||
curl \
|
curl \
|
||||||
gnupg \
|
gnupg \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
git-annex \
|
||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
RUN addgroup \
|
RUN addgroup \
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -8,7 +8,7 @@ self := $(location)
|
||||||
@tmpdir=`mktemp --tmpdir -d` ; \
|
@tmpdir=`mktemp --tmpdir -d` ; \
|
||||||
echo Using temporary directory $$tmpdir for test repositories ; \
|
echo Using temporary directory $$tmpdir for test repositories ; \
|
||||||
USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \
|
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
|
else
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ else
|
||||||
FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY}
|
FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY}
|
||||||
else
|
else
|
||||||
# drop the "g" prefix prepended by git describe to the commit hash
|
# 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
|
||||||
endif
|
endif
|
||||||
FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//')
|
FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//')
|
||||||
|
|
|
@ -1,27 +1,33 @@
|
||||||
<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
|
<!--
|
||||||
<style type="text/css">
|
This logo was created by Michael Hanke <mih@ngln.eu> from
|
||||||
circle {
|
the original Forgejo logo by Caesar Schinas and the git-annex
|
||||||
fill: none;
|
logo by Henrik Nyh <http://henrik.nyh.se/>, Joey Hess <id@joeyh.name>,
|
||||||
stroke: #000;
|
John Lawrence, and Yann Büchau <nobodyinperson at posteo de>.
|
||||||
stroke-width: 15;
|
|
||||||
}
|
It is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
||||||
path {
|
International (CC BY-SA 4.0) license.
|
||||||
fill: none;
|
-->
|
||||||
stroke: #000;
|
<svg xmlns="http://www.w3.org/2000/svg" width="212" height="212"
|
||||||
stroke-width: 25;
|
viewBox="0 0 56.092 56.092" xmlns:v="https://vecta.io/nano">
|
||||||
}
|
<g transform="matrix(1.003855 0 0 1.003855 -155.52693 -24.929635)"
|
||||||
.orange {
|
fill="none">
|
||||||
stroke:#ff6600;
|
<g stroke-width="6.615">
|
||||||
}
|
<path d="M168.804 70.908V44.979a13.229 13.229 0 0 1 13.229-13.229h5.292"
|
||||||
.red {
|
stroke="#f60" />
|
||||||
stroke:#d40000;
|
<path d="M168.804 70.908v-7.937a13.229 13.229 0 0 1 13.229-13.229h5.292"
|
||||||
}
|
stroke="#d40000" />
|
||||||
</style>
|
</g>
|
||||||
<g transform="translate(6,6)">
|
<g stroke-width="3.969">
|
||||||
<path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" class="orange" />
|
<circle cx="191.029" cy="31.75" r="4.762" stroke="#f60" />
|
||||||
<path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" class="red" />
|
<g stroke="#d40000">
|
||||||
<circle cx="142" cy="20" r="18" class="orange" />
|
<circle cx="191.029" cy="49.742" r="4.762" />
|
||||||
<circle cx="142" cy="88" r="18" class="red" />
|
<circle cx="168.804" cy="74.083" r="4.762" />
|
||||||
<circle cx="58" cy="180" r="18" class="red" />
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g fill="#777">
|
||||||
|
<path d="M34.648 56.182c-2.089-.269-4.238-2.244-4.538-4.561-.288-1.937.128-4.198 1.729-5.476.702-.479 1.658-1.015 2.5-.951v2.757c-1.003.112-1.975 1.252-1.954 2.296.025 1.359.626 2.607 1.933 3.014 1.446.487 3.102.348 4.44-.461 1.106-.862 1.208-2.466.63-3.687-.26-.672-.821-1.165-1.582-1.163v2.392H35.66v-5.149h6.5v1.852l-1.78.016c1.661.947 1.832 2.991 1.747 4.409.06 2.436-2.347 4.422-4.734 4.688-.864.065-1.594.068-2.743.023zm-4.644-12.011l-.014-2.702h12.185l-.001 2.715-12.17-.013zm4.645-3.651v-2.892h-4.724v-2.516h4.724v-3.245h2.826v3.245h4.702v2.516h-4.702l-.047 2.886-2.779.006z" />
|
||||||
|
<path d="M22.23 24.801l-2.819 3.94h1.565a9.01 9.01 0 0 0 .103 1.283l2.488-.377c-.044-.296-.071-.599-.075-.906h1.556zm1.531 5.732l-2.405.724a8.9 8.9 0 0 0 .447 1.18l2.281-1.055a6.36 6.36 0 0 1-.323-.848zm.753 1.622L22.4 33.518a8.99 8.99 0 0 0 3.949 3.365l1.006-2.305a6.45 6.45 0 0 1-2.839-2.421zm3.675 2.715l-.679 2.425a8.94 8.94 0 0 0 1.18.244l.348-2.492a6.52 6.52 0 0 1-.848-.178zm21.673-10.069l-2.819 3.94h1.565c-.003.308-.03.61-.075.906l2.488.377a9.01 9.01 0 0 0 .103-1.283h1.556zm-1.523 5.732a6.36 6.36 0 0 1-.323.848l2.281 1.055c.176-.379.325-.774.447-1.18zm-.753 1.622a6.45 6.45 0 0 1-2.839 2.421l1.006 2.305a8.99 8.99 0 0 0 3.949-3.365zm-3.675 2.715a6.52 6.52 0 0 1-.848.178l.348 2.492a8.94 8.94 0 0 0 1.18-.244z"
|
||||||
|
fill-rule="evenodd" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 2.4 KiB |
|
@ -1,27 +1,33 @@
|
||||||
<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
|
<!--
|
||||||
<style type="text/css">
|
This logo was created by Michael Hanke <mih@ngln.eu> from
|
||||||
circle {
|
the original Forgejo logo by Caesar Schinas and the git-annex
|
||||||
fill: none;
|
logo by Henrik Nyh <http://henrik.nyh.se/>, Joey Hess <id@joeyh.name>,
|
||||||
stroke: #000;
|
John Lawrence, and Yann Büchau <nobodyinperson at posteo de>.
|
||||||
stroke-width: 15;
|
|
||||||
}
|
It is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
||||||
path {
|
International (CC BY-SA 4.0) license.
|
||||||
fill: none;
|
-->
|
||||||
stroke: #000;
|
<svg xmlns="http://www.w3.org/2000/svg" width="212" height="212"
|
||||||
stroke-width: 25;
|
viewBox="0 0 56.092 56.092" xmlns:v="https://vecta.io/nano">
|
||||||
}
|
<g transform="matrix(1.003855 0 0 1.003855 -155.52693 -24.929635)"
|
||||||
.orange {
|
fill="none">
|
||||||
stroke:#ff6600;
|
<g stroke-width="6.615">
|
||||||
}
|
<path d="M168.804 70.908V44.979a13.229 13.229 0 0 1 13.229-13.229h5.292"
|
||||||
.red {
|
stroke="#f60" />
|
||||||
stroke:#d40000;
|
<path d="M168.804 70.908v-7.937a13.229 13.229 0 0 1 13.229-13.229h5.292"
|
||||||
}
|
stroke="#d40000" />
|
||||||
</style>
|
</g>
|
||||||
<g transform="translate(6,6)">
|
<g stroke-width="3.969">
|
||||||
<path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" class="orange" />
|
<circle cx="191.029" cy="31.75" r="4.762" stroke="#f60" />
|
||||||
<path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" class="red" />
|
<g stroke="#d40000">
|
||||||
<circle cx="142" cy="20" r="18" class="orange" />
|
<circle cx="191.029" cy="49.742" r="4.762" />
|
||||||
<circle cx="142" cy="88" r="18" class="red" />
|
<circle cx="168.804" cy="74.083" r="4.762" />
|
||||||
<circle cx="58" cy="180" r="18" class="red" />
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g fill="#777">
|
||||||
|
<path d="M34.648 56.182c-2.089-.269-4.238-2.244-4.538-4.561-.288-1.937.128-4.198 1.729-5.476.702-.479 1.658-1.015 2.5-.951v2.757c-1.003.112-1.975 1.252-1.954 2.296.025 1.359.626 2.607 1.933 3.014 1.446.487 3.102.348 4.44-.461 1.106-.862 1.208-2.466.63-3.687-.26-.672-.821-1.165-1.582-1.163v2.392H35.66v-5.149h6.5v1.852l-1.78.016c1.661.947 1.832 2.991 1.747 4.409.06 2.436-2.347 4.422-4.734 4.688-.864.065-1.594.068-2.743.023zm-4.644-12.011l-.014-2.702h12.185l-.001 2.715-12.17-.013zm4.645-3.651v-2.892h-4.724v-2.516h4.724v-3.245h2.826v3.245h4.702v2.516h-4.702l-.047 2.886-2.779.006z" />
|
||||||
|
<path d="M22.23 24.801l-2.819 3.94h1.565a9.01 9.01 0 0 0 .103 1.283l2.488-.377c-.044-.296-.071-.599-.075-.906h1.556zm1.531 5.732l-2.405.724a8.9 8.9 0 0 0 .447 1.18l2.281-1.055a6.36 6.36 0 0 1-.323-.848zm.753 1.622L22.4 33.518a8.99 8.99 0 0 0 3.949 3.365l1.006-2.305a6.45 6.45 0 0 1-2.839-2.421zm3.675 2.715l-.679 2.425a8.94 8.94 0 0 0 1.18.244l.348-2.492a6.52 6.52 0 0 1-.848-.178zm21.673-10.069l-2.819 3.94h1.565c-.003.308-.03.61-.075.906l2.488.377a9.01 9.01 0 0 0 .103-1.283h1.556zm-1.523 5.732a6.36 6.36 0 0 1-.323.848l2.281 1.055c.176-.379.325-.774.447-1.18zm-.753 1.622a6.45 6.45 0 0 1-2.839 2.421l1.006 2.305a8.99 8.99 0 0 0 3.949-3.365zm-3.675 2.715a6.52 6.52 0 0 1-.848.178l.348 2.492a8.94 8.94 0 0 0 1.18-.244z"
|
||||||
|
fill-rule="evenodd" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 2.4 KiB |
76
cmd/serv.go
76
cmd/serv.go
|
@ -38,6 +38,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
lfsAuthenticateVerb = "git-lfs-authenticate"
|
lfsAuthenticateVerb = "git-lfs-authenticate"
|
||||||
|
gitAnnexShellVerb = "git-annex-shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CmdServ represents the available serv sub-command.
|
// CmdServ represents the available serv sub-command.
|
||||||
|
@ -79,6 +80,7 @@ var (
|
||||||
"git-upload-archive": perm.AccessModeRead,
|
"git-upload-archive": perm.AccessModeRead,
|
||||||
"git-receive-pack": perm.AccessModeWrite,
|
"git-receive-pack": perm.AccessModeWrite,
|
||||||
lfsAuthenticateVerb: perm.AccessModeNone,
|
lfsAuthenticateVerb: perm.AccessModeNone,
|
||||||
|
gitAnnexShellVerb: perm.AccessModeNone, // annex permissions are enforced by GIT_ANNEX_SHELL_READONLY, rather than the Gitea API
|
||||||
}
|
}
|
||||||
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
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)
|
rr := strings.SplitN(repoPath, "/", 2)
|
||||||
if len(rr) != 2 {
|
if len(rr) != 2 {
|
||||||
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
|
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.
|
// so that username and reponame are not affected.
|
||||||
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
|
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) {
|
if alphaDashDotPattern.MatchString(reponame) {
|
||||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
||||||
}
|
}
|
||||||
|
@ -303,21 +339,45 @@ func runServ(c *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var gitcmd *exec.Cmd
|
gitBinVerb, err := exec.LookPath(verb)
|
||||||
gitBinPath := filepath.Dir(git.GitExecutable) // e.g. /usr/bin
|
if err != nil {
|
||||||
gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
|
|
||||||
if _, err := os.Stat(gitBinVerb); err != nil {
|
|
||||||
// if the command "git-upload-pack" doesn't exist, try to split "git-upload-pack" to use the sub-command with git
|
// 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: 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)
|
verbFields := strings.SplitN(verb, "-", 2)
|
||||||
if len(verbFields) == 2 {
|
if len(verbFields) == 2 {
|
||||||
// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
|
// 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)
|
// by default, use the verb (it has been checked above by allowedCommands)
|
||||||
gitcmd = exec.CommandContext(ctx, gitBinVerb, repoPath)
|
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)
|
process.SetSysProcAttribute(gitcmd)
|
||||||
|
|
11
cmd/web.go
11
cmd/web.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -258,6 +259,12 @@ func runWeb(ctx *cli.Context) error {
|
||||||
createPIDFile(ctx.String("pid"))
|
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 !setting.InstallLock {
|
||||||
if err := serveInstall(ctx); err != nil {
|
if err := serveInstall(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -322,6 +329,10 @@ func listen(m http.Handler, handleRedirector bool) error {
|
||||||
log.Info("LFS server enabled")
|
log.Info("LFS server enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if setting.Annex.Enabled {
|
||||||
|
log.Info("git-annex enabled")
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
switch setting.Protocol {
|
switch setting.Protocol {
|
||||||
case setting.HTTP:
|
case setting.HTTP:
|
||||||
|
|
|
@ -2678,6 +2678,17 @@ LEVEL = Info
|
||||||
;; Limit the number of concurrent upload/download operations within a batch
|
;; Limit the number of concurrent upload/download operations within a batch
|
||||||
;BATCH_OPERATION_CONCURRENCY = 8
|
;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
|
;; 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"
|
||||||
|
|
||||||
|
"forgejo.org/modules/git"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
"forgejo.org/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"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
|
|
||||||
|
@ -101,6 +102,12 @@ func Int64sToStrings(ints []int64) []string {
|
||||||
|
|
||||||
// EntryIcon returns the octicon class for displaying files/directories
|
// EntryIcon returns the octicon class for displaying files/directories
|
||||||
func EntryIcon(entry *git.TreeEntry) string {
|
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 {
|
switch {
|
||||||
case entry.IsLink():
|
case entry.IsLink():
|
||||||
te, _, err := entry.FollowLink()
|
te, _, err := entry.FollowLink()
|
||||||
|
|
|
@ -126,6 +126,10 @@ func (b *blobReader) Close() error {
|
||||||
return nil
|
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)
|
// Name returns name of the tree entry this blob object was created from (or empty string)
|
||||||
func (b *Blob) Name() string {
|
func (b *Blob) Name() string {
|
||||||
return b.name
|
return b.name
|
||||||
|
|
|
@ -447,12 +447,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
|
// 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 {
|
func AllowLFSFiltersArgs() TrustedCmdArgs {
|
||||||
// Now here we should explicitly allow lfs filters to run
|
// Now here we should explicitly allow lfs filters to run
|
||||||
filteredLFSGlobalArgs := make(TrustedCmdArgs, len(globalCommandArgs))
|
filteredLFSGlobalArgs := make(TrustedCmdArgs, len(globalCommandArgs))
|
||||||
j := 0
|
j := 0
|
||||||
for _, arg := range globalCommandArgs {
|
for _, arg := range globalCommandArgs {
|
||||||
if strings.Contains(string(arg), "lfs") {
|
if strings.Contains(string(arg), "lfs") || strings.Contains(string(arg), "credential") {
|
||||||
j--
|
j--
|
||||||
} else {
|
} else {
|
||||||
filteredLFSGlobalArgs[j] = arg
|
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
|
@ -11,6 +11,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/graceful"
|
"forgejo.org/modules/graceful"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/modules/log"
|
||||||
"forgejo.org/modules/markup"
|
"forgejo.org/modules/markup"
|
||||||
|
@ -82,8 +83,22 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||||
commands = strings.Fields(command)
|
commands = strings.Fields(command)
|
||||||
args = commands[1:]
|
args = commands[1:]
|
||||||
)
|
)
|
||||||
|
isAnnexed, _ := annex.IsAnnexed(ctx.Blob)
|
||||||
if p.IsInputFile {
|
// 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
|
// write to temp file
|
||||||
f, err := os.CreateTemp("", "gitea_input")
|
f, err := os.CreateTemp("", "gitea_input")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -126,6 +141,12 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||||
os.Environ(),
|
os.Environ(),
|
||||||
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
|
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
|
||||||
"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
|
"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 {
|
if !p.IsInputFile {
|
||||||
cmd.Stdin = input
|
cmd.Stdin = input
|
||||||
|
|
|
@ -67,14 +67,18 @@ type Header struct {
|
||||||
|
|
||||||
// RenderContext represents a render context
|
// RenderContext represents a render context
|
||||||
type RenderContext struct {
|
type RenderContext struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
RelativePath string // relative path from tree root of the branch
|
RelativePath string // relative path from tree root of the branch
|
||||||
Type string
|
Type string
|
||||||
IsWiki bool
|
IsWiki bool
|
||||||
Links Links
|
Links Links
|
||||||
Metas map[string]string
|
Metas map[string]string
|
||||||
DefaultLink string
|
DefaultLink string
|
||||||
GitRepo *git.Repository
|
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
|
ShaExistCache map[string]bool
|
||||||
cancelFn func()
|
cancelFn func()
|
||||||
SidebarTocNode ast.Node
|
SidebarTocNode ast.Node
|
||||||
|
|
|
@ -40,6 +40,7 @@ type ServCommandResults struct {
|
||||||
UserName string
|
UserName string
|
||||||
UserEmail string
|
UserEmail string
|
||||||
UserID int64
|
UserID int64
|
||||||
|
UserMode perm.AccessMode
|
||||||
OwnerName string
|
OwnerName string
|
||||||
RepoName string
|
RepoName string
|
||||||
RepoID int64
|
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 (
|
||||||
|
"forgejo.org/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
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,6 +148,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||||
loadCamoFrom(cfg)
|
loadCamoFrom(cfg)
|
||||||
loadI18nFrom(cfg)
|
loadI18nFrom(cfg)
|
||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
|
loadAnnexFrom(cfg)
|
||||||
loadMirrorFrom(cfg)
|
loadMirrorFrom(cfg)
|
||||||
loadMarkupFrom(cfg)
|
loadMarkupFrom(cfg)
|
||||||
loadQuotaFrom(cfg)
|
loadQuotaFrom(cfg)
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -32,10 +34,48 @@ func Remove(name string) error {
|
||||||
return err
|
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 {
|
func RemoveAll(name string) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
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)
|
err = os.RemoveAll(name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
|
|
|
@ -1349,6 +1349,7 @@ view_git_blame=Zobrazit git blame
|
||||||
video_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „video“.
|
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“.
|
audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku HTML5 „audio“.
|
||||||
stored_lfs=Uloženo pomocí Git LFS
|
stored_lfs=Uloženo pomocí Git LFS
|
||||||
|
stored_annex=Uloženo pomocí Git Annex
|
||||||
symbolic_link=Symbolický odkaz
|
symbolic_link=Symbolický odkaz
|
||||||
executable_file=Spustitelný soubor
|
executable_file=Spustitelný soubor
|
||||||
vendored = Vendorováno
|
vendored = Vendorováno
|
||||||
|
@ -1374,6 +1375,7 @@ editor.upload_file=Nahrát soubor
|
||||||
editor.edit_file=Upravit soubor
|
editor.edit_file=Upravit soubor
|
||||||
editor.preview_changes=Náhled změn
|
editor.preview_changes=Náhled změn
|
||||||
editor.cannot_edit_lfs_files=Soubory LFS nemohou být upravovány přes webové rozhraní.
|
editor.cannot_edit_lfs_files=Soubory LFS 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.cannot_edit_non_text_files=Binární soubory nemohou být upravovány přes webové rozhraní.
|
||||||
editor.edit_this_file=Upravit soubor
|
editor.edit_this_file=Upravit soubor
|
||||||
editor.this_file_locked=Soubor je uzamčen
|
editor.this_file_locked=Soubor je uzamčen
|
||||||
|
|
|
@ -1349,6 +1349,8 @@ view_git_blame=„git blame“ ansehen
|
||||||
video_not_supported_in_browser=Dein Browser unterstützt das HTML5-„video“-Tag nicht.
|
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.
|
audio_not_supported_in_browser=Dein Browser unterstützt das HTML5-„audio“-Tag nicht.
|
||||||
stored_lfs=Gespeichert mit Git LFS
|
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
|
symbolic_link=Softlink
|
||||||
executable_file=Ausführbare Datei
|
executable_file=Ausführbare Datei
|
||||||
commit_graph=Commit-Graph
|
commit_graph=Commit-Graph
|
||||||
|
@ -1372,6 +1374,7 @@ editor.upload_file=Datei hochladen
|
||||||
editor.edit_file=Datei bearbeiten
|
editor.edit_file=Datei bearbeiten
|
||||||
editor.preview_changes=Vorschau der Änderungen
|
editor.preview_changes=Vorschau der Änderungen
|
||||||
editor.cannot_edit_lfs_files=LFS-Dateien können im Webinterface nicht bearbeitet werden.
|
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.cannot_edit_non_text_files=Binärdateien können nicht im Webinterface bearbeitet werden.
|
||||||
editor.edit_this_file=Datei bearbeiten
|
editor.edit_this_file=Datei bearbeiten
|
||||||
editor.this_file_locked=Datei ist gesperrt
|
editor.this_file_locked=Datei ist gesperrt
|
||||||
|
|
|
@ -1321,6 +1321,7 @@ view_git_blame=Προβολή git blame
|
||||||
video_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «video».
|
video_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «video».
|
||||||
audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «audio».
|
audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 «audio».
|
||||||
stored_lfs=Αποθηκεύτηκε με το Git LFS
|
stored_lfs=Αποθηκεύτηκε με το Git LFS
|
||||||
|
stored_annex=Αποθηκεύτηκε με το Git Annex
|
||||||
symbolic_link=Symbolic link
|
symbolic_link=Symbolic link
|
||||||
executable_file=Εκτελέσιμο αρχείο
|
executable_file=Εκτελέσιμο αρχείο
|
||||||
commit_graph=Γράφημα commit
|
commit_graph=Γράφημα commit
|
||||||
|
@ -1344,6 +1345,7 @@ editor.upload_file=Ανέβασμα αρχείου
|
||||||
editor.edit_file=Επεξεργασία αρχείου
|
editor.edit_file=Επεξεργασία αρχείου
|
||||||
editor.preview_changes=Προεπισκόπηση αλλαγών
|
editor.preview_changes=Προεπισκόπηση αλλαγών
|
||||||
editor.cannot_edit_lfs_files=Τα αρχεία LFS δεν μπορούν να επεξεργαστούν στη διεπαφή web.
|
editor.cannot_edit_lfs_files=Τα αρχεία LFS δεν μπορούν να επεξεργαστούν στη διεπαφή web.
|
||||||
|
editor.cannot_edit_annex_files=Τα αρχεία Annex δεν μπορούν να επεξεργαστούν στη διεπαφή web.
|
||||||
editor.cannot_edit_non_text_files=Τα δυαδικά αρχεία δεν μπορούν να επεξεργαστούν στη διεπαφή web.
|
editor.cannot_edit_non_text_files=Τα δυαδικά αρχεία δεν μπορούν να επεξεργαστούν στη διεπαφή web.
|
||||||
editor.edit_this_file=Επεξεργασία αρχείου
|
editor.edit_this_file=Επεξεργασία αρχείου
|
||||||
editor.this_file_locked=Το αρχείο είναι κλειδωμένο
|
editor.this_file_locked=Το αρχείο είναι κλειδωμένο
|
||||||
|
|
|
@ -1367,6 +1367,8 @@ view_git_blame = View git blame
|
||||||
video_not_supported_in_browser = Your browser does not support the HTML5 "video" tag.
|
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.
|
audio_not_supported_in_browser = Your browser does not support the HTML5 "audio" tag.
|
||||||
stored_lfs = Stored with Git LFS
|
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
|
symbolic_link = Symbolic link
|
||||||
executable_file = Executable file
|
executable_file = Executable file
|
||||||
vendored = Vendored
|
vendored = Vendored
|
||||||
|
@ -1394,6 +1396,7 @@ editor.upload_file = Upload file
|
||||||
editor.edit_file = Edit file
|
editor.edit_file = Edit file
|
||||||
editor.preview_changes = Preview changes
|
editor.preview_changes = Preview changes
|
||||||
editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface.
|
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.cannot_edit_non_text_files = Binary files cannot be edited in the web interface.
|
||||||
editor.edit_this_file = Edit file
|
editor.edit_this_file = Edit file
|
||||||
editor.this_file_locked = File is locked
|
editor.this_file_locked = File is locked
|
||||||
|
|
|
@ -1340,6 +1340,7 @@ view_git_blame=Ver Git blame
|
||||||
video_not_supported_in_browser=Su navegador no soporta el tag "video" de HTML5.
|
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.
|
audio_not_supported_in_browser=Su navegador no soporta el tag "audio" de HTML5.
|
||||||
stored_lfs=Almacenados con Git LFS
|
stored_lfs=Almacenados con Git LFS
|
||||||
|
stored_annex=Almacenados con Git Annex
|
||||||
symbolic_link=Enlace simbólico
|
symbolic_link=Enlace simbólico
|
||||||
executable_file=Archivo ejecutable
|
executable_file=Archivo ejecutable
|
||||||
commit_graph=Gráfico de commits
|
commit_graph=Gráfico de commits
|
||||||
|
@ -1363,6 +1364,7 @@ editor.upload_file=Subir archivo
|
||||||
editor.edit_file=Editar archivo
|
editor.edit_file=Editar archivo
|
||||||
editor.preview_changes=Vista previa de los cambios
|
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_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.cannot_edit_non_text_files=Los archivos binarios no se pueden editar en la interfaz web.
|
||||||
editor.edit_this_file=Editar archivo
|
editor.edit_this_file=Editar archivo
|
||||||
editor.this_file_locked=El archivo está bloqueado
|
editor.this_file_locked=El archivo está bloqueado
|
||||||
|
|
|
@ -1036,6 +1036,7 @@ file_copy_permalink=پرمالینک را کپی کنید
|
||||||
video_not_supported_in_browser=مرورگر شما از تگ video که در HTML5 تعریف شده است، پشتیبانی نمی کند.
|
video_not_supported_in_browser=مرورگر شما از تگ video که در HTML5 تعریف شده است، پشتیبانی نمی کند.
|
||||||
audio_not_supported_in_browser=مرورگر شما از تگ audio که در HTML5 تعریف شده است، پشتیبانی نمی کند.
|
audio_not_supported_in_browser=مرورگر شما از تگ audio که در HTML5 تعریف شده است، پشتیبانی نمی کند.
|
||||||
stored_lfs=ذخیره شده با GIT LFS
|
stored_lfs=ذخیره شده با GIT LFS
|
||||||
|
stored_annex=ذخیره شده با GIT Annex
|
||||||
symbolic_link=پیوند نمادین
|
symbolic_link=پیوند نمادین
|
||||||
commit_graph=نمودار کامیت
|
commit_graph=نمودار کامیت
|
||||||
commit_graph.select=انتخاب برنچها
|
commit_graph.select=انتخاب برنچها
|
||||||
|
@ -1053,6 +1054,7 @@ editor.upload_file=بارگذاری پرونده
|
||||||
editor.edit_file=ویرایش پرونده
|
editor.edit_file=ویرایش پرونده
|
||||||
editor.preview_changes=پیش نمایش تغییرات
|
editor.preview_changes=پیش نمایش تغییرات
|
||||||
editor.cannot_edit_lfs_files=پرونده های LFS در صحفه وب قابل تغییر نیست.
|
editor.cannot_edit_lfs_files=پرونده های LFS در صحفه وب قابل تغییر نیست.
|
||||||
|
editor.cannot_edit_annex_files=پرونده های Annex در صحفه وب قابل تغییر نیست.
|
||||||
editor.cannot_edit_non_text_files=پروندههای دودویی در صفحه وب قابل تغییر نیست.
|
editor.cannot_edit_non_text_files=پروندههای دودویی در صفحه وب قابل تغییر نیست.
|
||||||
editor.edit_this_file=ویرایش پرونده
|
editor.edit_this_file=ویرایش پرونده
|
||||||
editor.this_file_locked=پرونده قفل شده است
|
editor.this_file_locked=پرونده قفل شده است
|
||||||
|
|
|
@ -1351,6 +1351,7 @@ view_git_blame=Voir Git blame
|
||||||
video_not_supported_in_browser=Votre navigateur ne supporte pas la balise « vidéo » HTML5.
|
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.
|
audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « audio » HTML5.
|
||||||
stored_lfs=Stocké avec Git LFS
|
stored_lfs=Stocké avec Git LFS
|
||||||
|
stored_annex=Stocké avec Git Annex
|
||||||
symbolic_link=Lien symbolique
|
symbolic_link=Lien symbolique
|
||||||
executable_file=Fichier exécutable
|
executable_file=Fichier exécutable
|
||||||
vendored = Vendored
|
vendored = Vendored
|
||||||
|
@ -1376,6 +1377,7 @@ editor.upload_file=Téléverser un fichier
|
||||||
editor.edit_file=Modifier le fichier
|
editor.edit_file=Modifier le fichier
|
||||||
editor.preview_changes=Aperçu des modifications
|
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_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.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.edit_this_file=Modifier le fichier
|
||||||
editor.this_file_locked=Le fichier est verrouillé
|
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.
|
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.
|
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_lfs=Git LFS-el eltárolva
|
||||||
|
stored_annex=Git Annex-el eltárolva
|
||||||
symbolic_link=Szimbolikus hivatkozás
|
symbolic_link=Szimbolikus hivatkozás
|
||||||
commit_graph=Commit gráf
|
commit_graph=Commit gráf
|
||||||
commit_graph.hide_pr_refs=Pull request-ek elrejtése
|
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.edit_file=Fájl szerkesztése
|
||||||
editor.preview_changes=Változások előnézete
|
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_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.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.edit_this_file=Fájl szerkesztése
|
||||||
editor.this_file_locked=Zárolt állomány
|
editor.this_file_locked=Zárolt állomány
|
||||||
|
|
|
@ -681,6 +681,7 @@ file_permalink=Permalink
|
||||||
file_too_large=Berkas terlalu besar untuk ditampilkan.
|
file_too_large=Berkas terlalu besar untuk ditampilkan.
|
||||||
|
|
||||||
stored_lfs=Tersimpan dengan GIT LFS
|
stored_lfs=Tersimpan dengan GIT LFS
|
||||||
|
stored_annex=Tersimpan dengan GIT Annex
|
||||||
commit_graph=Grafik Komit
|
commit_graph=Grafik Komit
|
||||||
blame=Salahkan
|
blame=Salahkan
|
||||||
normal_view=Pandangan Normal
|
normal_view=Pandangan Normal
|
||||||
|
@ -692,6 +693,7 @@ editor.upload_file=Unggah Berkas
|
||||||
editor.edit_file=Sunting Berkas
|
editor.edit_file=Sunting Berkas
|
||||||
editor.preview_changes=Tinjau Perubahan
|
editor.preview_changes=Tinjau Perubahan
|
||||||
editor.cannot_edit_lfs_files=Berkas LFS tidak dapat disunting dalam antarmuka web.
|
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.cannot_edit_non_text_files=Berkas biner tidak dapat disunting dalam antarmuka web.
|
||||||
editor.edit_this_file=Sunting Berkas
|
editor.edit_this_file=Sunting Berkas
|
||||||
editor.this_file_locked=Berkas terkunci
|
editor.this_file_locked=Berkas terkunci
|
||||||
|
|
|
@ -684,6 +684,7 @@ file_view_rendered=Skoða Unnið
|
||||||
|
|
||||||
file_copy_permalink=Afrita Varanlega Slóð
|
file_copy_permalink=Afrita Varanlega Slóð
|
||||||
stored_lfs=Geymt með Git LFS
|
stored_lfs=Geymt með Git LFS
|
||||||
|
stored_annex=Geymt með Git Annex
|
||||||
commit_graph.hide_pr_refs=Fela Sameiningarbeiðnir
|
commit_graph.hide_pr_refs=Fela Sameiningarbeiðnir
|
||||||
commit_graph.monochrome=Einlitað
|
commit_graph.monochrome=Einlitað
|
||||||
commit_graph.color=Litað
|
commit_graph.color=Litað
|
||||||
|
|
|
@ -1277,6 +1277,7 @@ view_git_blame=Visualizza git incolpa
|
||||||
video_not_supported_in_browser=Il tuo browser non supporta le etichette "video" di HTML5.
|
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.
|
audio_not_supported_in_browser=Il tuo browser non supporta le etichette "audio" di HTML5.
|
||||||
stored_lfs=Memorizzati con Git LFS
|
stored_lfs=Memorizzati con Git LFS
|
||||||
|
stored_annex=Memorizzati con Git Annex
|
||||||
symbolic_link=Link Simbolico
|
symbolic_link=Link Simbolico
|
||||||
commit_graph=Grafico dei commit
|
commit_graph=Grafico dei commit
|
||||||
commit_graph.select=Seleziona rami
|
commit_graph.select=Seleziona rami
|
||||||
|
@ -1295,6 +1296,7 @@ editor.upload_file=Carica file
|
||||||
editor.edit_file=Modifica file
|
editor.edit_file=Modifica file
|
||||||
editor.preview_changes=Anteprima modifiche
|
editor.preview_changes=Anteprima modifiche
|
||||||
editor.cannot_edit_lfs_files=I file LFS non possono essere modificati nell'interfaccia web.
|
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.cannot_edit_non_text_files=I file binari non possono essere modificati tramite interfaccia web.
|
||||||
editor.edit_this_file=Modifica file
|
editor.edit_this_file=Modifica file
|
||||||
editor.this_file_locked=Il file è bloccato
|
editor.this_file_locked=Il file è bloccato
|
||||||
|
|
|
@ -1307,6 +1307,7 @@ view_git_blame=Git Blameを表示
|
||||||
video_not_supported_in_browser=このブラウザはHTML5のvideoタグをサポートしていません。
|
video_not_supported_in_browser=このブラウザはHTML5のvideoタグをサポートしていません。
|
||||||
audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサポートしていません。
|
audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサポートしていません。
|
||||||
stored_lfs=Git LFSで保管されています
|
stored_lfs=Git LFSで保管されています
|
||||||
|
stored_annex=Git Annexで保管されています
|
||||||
symbolic_link=シンボリック リンク
|
symbolic_link=シンボリック リンク
|
||||||
executable_file=実行ファイル
|
executable_file=実行ファイル
|
||||||
commit_graph=コミットグラフ
|
commit_graph=コミットグラフ
|
||||||
|
@ -1330,6 +1331,7 @@ editor.upload_file=ファイルをアップロード
|
||||||
editor.edit_file=ファイルを編集
|
editor.edit_file=ファイルを編集
|
||||||
editor.preview_changes=変更をプレビュー
|
editor.preview_changes=変更をプレビュー
|
||||||
editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。
|
editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。
|
||||||
|
editor.cannot_edit_annex_files=AnnexのファイルはWebインターフェースで編集できません。
|
||||||
editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。
|
editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。
|
||||||
editor.edit_this_file=ファイルを編集
|
editor.edit_this_file=ファイルを編集
|
||||||
editor.this_file_locked=ファイルはロックされています
|
editor.this_file_locked=ファイルはロックされています
|
||||||
|
|
|
@ -811,6 +811,7 @@ file_too_large=보여주기에는 파일이 너무 큽니다.
|
||||||
video_not_supported_in_browser=당신의 브라우저가 HTML5의 "video" 태그를 지원하지 않습니다.
|
video_not_supported_in_browser=당신의 브라우저가 HTML5의 "video" 태그를 지원하지 않습니다.
|
||||||
audio_not_supported_in_browser=당신의 브라우저가 HTML5의 "audio" 태그를 지원하지 않습니다.
|
audio_not_supported_in_browser=당신의 브라우저가 HTML5의 "audio" 태그를 지원하지 않습니다.
|
||||||
stored_lfs=Git LFS에 저장되어 있습니다
|
stored_lfs=Git LFS에 저장되어 있습니다
|
||||||
|
stored_annex=Git Annex에 저장되어 있습니다
|
||||||
commit_graph=커밋 그래프
|
commit_graph=커밋 그래프
|
||||||
|
|
||||||
editor.new_file=새 파일
|
editor.new_file=새 파일
|
||||||
|
|
|
@ -1348,6 +1348,7 @@ view_git_blame=Apskatīt Git izmaiņu veicējus
|
||||||
video_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "video".
|
video_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "video".
|
||||||
audio_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "audio".
|
audio_not_supported_in_browser=Pārlūks neatbalsta HTML5 tagu "audio".
|
||||||
stored_lfs=Saglabāts Git LFS
|
stored_lfs=Saglabāts Git LFS
|
||||||
|
stored_annex=Saglabāts Git Annex
|
||||||
symbolic_link=Simboliska saite
|
symbolic_link=Simboliska saite
|
||||||
executable_file=Izpildāma datne
|
executable_file=Izpildāma datne
|
||||||
commit_graph=Iesūtījumu karte
|
commit_graph=Iesūtījumu karte
|
||||||
|
@ -1371,6 +1372,7 @@ editor.upload_file=Augšupielādēt datni
|
||||||
editor.edit_file=Labot datni
|
editor.edit_file=Labot datni
|
||||||
editor.preview_changes=Priekšskatīt izmaiņas
|
editor.preview_changes=Priekšskatīt izmaiņas
|
||||||
editor.cannot_edit_lfs_files=LFS datnes nevar labot tīmekļa saskarnē.
|
editor.cannot_edit_lfs_files=LFS datnes nevar labot tīmekļa saskarnē.
|
||||||
|
editor.cannot_edit_annex_files=Annex datnes tīmekļa saskarnē nevar labot.
|
||||||
editor.cannot_edit_non_text_files=Binārās datnes nevar labot tīmekļa saskarnē.
|
editor.cannot_edit_non_text_files=Binārās datnes nevar labot tīmekļa saskarnē.
|
||||||
editor.edit_this_file=Labot datni
|
editor.edit_this_file=Labot datni
|
||||||
editor.this_file_locked=Datne ir slēgta
|
editor.this_file_locked=Datne ir slēgta
|
||||||
|
@ -4072,4 +4074,4 @@ filepreview.lines = %[1]d. līdz %[2]d. rinda %[3]s
|
||||||
filepreview.truncated = Priekšskatījums tika saīsināts
|
filepreview.truncated = Priekšskatījums tika saīsināts
|
||||||
|
|
||||||
[translation_meta]
|
[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.
|
||||||
|
|
|
@ -1316,6 +1316,7 @@ view_git_blame=Bekijk git blame
|
||||||
video_not_supported_in_browser=Uw browser ondersteunt de HTML5 "video" element niet.
|
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.
|
audio_not_supported_in_browser=Uw browser ondersteunt de HTML5 "audio" element niet.
|
||||||
stored_lfs=Opgeslagen met Git LFS
|
stored_lfs=Opgeslagen met Git LFS
|
||||||
|
stored_annex=Opgeslagen met Git Annex
|
||||||
symbolic_link=Symbolische link
|
symbolic_link=Symbolische link
|
||||||
commit_graph=Commit grafiek
|
commit_graph=Commit grafiek
|
||||||
commit_graph.select=Selecteer branches
|
commit_graph.select=Selecteer branches
|
||||||
|
@ -1334,6 +1335,7 @@ editor.upload_file=Upload bestand
|
||||||
editor.edit_file=Bewerk bestand
|
editor.edit_file=Bewerk bestand
|
||||||
editor.preview_changes=Voorbeeld tonen
|
editor.preview_changes=Voorbeeld tonen
|
||||||
editor.cannot_edit_lfs_files=LFS-bestanden kunnen niet worden bewerkt in de webinterface.
|
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.cannot_edit_non_text_files=Binaire bestanden kunnen niet worden bewerkt in de webinterface.
|
||||||
editor.edit_this_file=Bewerk bestand
|
editor.edit_this_file=Bewerk bestand
|
||||||
editor.this_file_locked=Bestand is vergrendeld
|
editor.this_file_locked=Bestand is vergrendeld
|
||||||
|
|
|
@ -1255,6 +1255,7 @@ file_copy_permalink=Kopiuj bezpośredni odnośnik
|
||||||
video_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "video".
|
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".
|
audio_not_supported_in_browser=Twoja przeglądarka nie obsługuje znacznika HTML5 "audio".
|
||||||
stored_lfs=Przechowane za pomocą Git LFS
|
stored_lfs=Przechowane za pomocą Git LFS
|
||||||
|
stored_annex=Przechowane za pomocą Git Annex
|
||||||
symbolic_link=Dowiązanie symboliczne
|
symbolic_link=Dowiązanie symboliczne
|
||||||
commit_graph=Wykres commitów
|
commit_graph=Wykres commitów
|
||||||
commit_graph.select=Wybierz gałęzie
|
commit_graph.select=Wybierz gałęzie
|
||||||
|
@ -1272,6 +1273,7 @@ editor.upload_file=Wyślij plik
|
||||||
editor.edit_file=Edytuj plik
|
editor.edit_file=Edytuj plik
|
||||||
editor.preview_changes=Podgląd zmian
|
editor.preview_changes=Podgląd zmian
|
||||||
editor.cannot_edit_lfs_files=Pliki LFS nie mogą być edytowane poprzez interfejs przeglądarkowy.
|
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.cannot_edit_non_text_files=Pliki binarne nie mogą być edytowane poprzez interfejs przeglądarkowy.
|
||||||
editor.edit_this_file=Edytuj plik
|
editor.edit_this_file=Edytuj plik
|
||||||
editor.this_file_locked=Plik jest zablokowany
|
editor.this_file_locked=Plik jest zablokowany
|
||||||
|
|
|
@ -1341,6 +1341,7 @@ view_git_blame=Ver git blame
|
||||||
video_not_supported_in_browser=Seu navegador não tem suporte para a tag "video" do HTML5.
|
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.
|
audio_not_supported_in_browser=Seu navegador não tem suporte para a tag "audio" do HTML5.
|
||||||
stored_lfs=Armazenado com Git LFS
|
stored_lfs=Armazenado com Git LFS
|
||||||
|
stored_annex=Armazenado com Git Annex
|
||||||
symbolic_link=Link simbólico
|
symbolic_link=Link simbólico
|
||||||
executable_file=Arquivo executável
|
executable_file=Arquivo executável
|
||||||
commit_graph=Gráfico de commits
|
commit_graph=Gráfico de commits
|
||||||
|
@ -1364,6 +1365,7 @@ editor.upload_file=Enviar arquivo
|
||||||
editor.edit_file=Editar arquivo
|
editor.edit_file=Editar arquivo
|
||||||
editor.preview_changes=Pré-visualizar alterações
|
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_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.cannot_edit_non_text_files=Arquivos binários não podem ser editados na interface web.
|
||||||
editor.edit_this_file=Editar arquivo
|
editor.edit_this_file=Editar arquivo
|
||||||
editor.this_file_locked=Arquivo está bloqueado
|
editor.this_file_locked=Arquivo está bloqueado
|
||||||
|
|
|
@ -1353,6 +1353,7 @@ view_git_blame=Ver git blame
|
||||||
video_not_supported_in_browser=O seu navegador não suporta a etiqueta "video" do HTML5.
|
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.
|
audio_not_supported_in_browser=O seu navegador não suporta a etiqueta "audio" do HTML5.
|
||||||
stored_lfs=Armazenado com Git LFS
|
stored_lfs=Armazenado com Git LFS
|
||||||
|
stored_annex=Armazenado com Git Annex
|
||||||
symbolic_link=Ligação simbólica
|
symbolic_link=Ligação simbólica
|
||||||
executable_file=Ficheiro executável
|
executable_file=Ficheiro executável
|
||||||
vendored=Externo
|
vendored=Externo
|
||||||
|
@ -1378,6 +1379,7 @@ editor.upload_file=Carregar ficheiro
|
||||||
editor.edit_file=Editar ficheiro
|
editor.edit_file=Editar ficheiro
|
||||||
editor.preview_changes=Pré-visualizar modificações
|
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_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.cannot_edit_non_text_files=Ficheiros binários não podem ser editados na interface da web.
|
||||||
editor.edit_this_file=Editar ficheiro
|
editor.edit_this_file=Editar ficheiro
|
||||||
editor.this_file_locked=Ficheiro bloqueado
|
editor.this_file_locked=Ficheiro bloqueado
|
||||||
|
|
|
@ -1336,6 +1336,7 @@ view_git_blame=Показать git blame
|
||||||
video_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «video».
|
video_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «video».
|
||||||
audio_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «audio».
|
audio_not_supported_in_browser=Ваш браузер не поддерживает тэг HTML5 «audio».
|
||||||
stored_lfs=Хранится Git LFS
|
stored_lfs=Хранится Git LFS
|
||||||
|
stored_annex=Хранится Git Annex
|
||||||
symbolic_link=Символическая ссылка
|
symbolic_link=Символическая ссылка
|
||||||
executable_file=Исполняемый файл
|
executable_file=Исполняемый файл
|
||||||
commit_graph=Граф коммитов
|
commit_graph=Граф коммитов
|
||||||
|
@ -1359,6 +1360,7 @@ editor.upload_file=Загрузить файл
|
||||||
editor.edit_file=Редактировать файл
|
editor.edit_file=Редактировать файл
|
||||||
editor.preview_changes=Просмотр изменений
|
editor.preview_changes=Просмотр изменений
|
||||||
editor.cannot_edit_lfs_files=LFS файлы невозможно редактировать в веб-интерфейсе.
|
editor.cannot_edit_lfs_files=LFS файлы невозможно редактировать в веб-интерфейсе.
|
||||||
|
editor.cannot_edit_annex_files=Annex файлы невозможно редактировать в веб-интерфейсе.
|
||||||
editor.cannot_edit_non_text_files=Двоичные файлы нельзя редактировать в веб-интерфейсе.
|
editor.cannot_edit_non_text_files=Двоичные файлы нельзя редактировать в веб-интерфейсе.
|
||||||
editor.edit_this_file=Редактировать файл
|
editor.edit_this_file=Редактировать файл
|
||||||
editor.this_file_locked=Файл заблокирован
|
editor.this_file_locked=Файл заблокирован
|
||||||
|
|
|
@ -894,6 +894,7 @@ file_copy_permalink=පිටපත් මාමලින්ක්
|
||||||
video_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'වීඩියෝ' ටැගය සඳහා සහය නොදක්වයි.
|
video_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'වීඩියෝ' ටැගය සඳහා සහය නොදක්වයි.
|
||||||
audio_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'ශ්රව්ය' ටැගය සඳහා සහය නොදක්වයි.
|
audio_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'ශ්රව්ය' ටැගය සඳහා සහය නොදක්වයි.
|
||||||
stored_lfs=Git LFS සමඟ ගබඩා
|
stored_lfs=Git LFS සමඟ ගබඩා
|
||||||
|
stored_annex=Git Annex සමඟ ගබඩා
|
||||||
symbolic_link=සංකේතාත්මක සබැඳිය
|
symbolic_link=සංකේතාත්මක සබැඳිය
|
||||||
commit_graph=ප්රස්තාරය කැප
|
commit_graph=ප්රස්තාරය කැප
|
||||||
commit_graph.select=ශාඛා තෝරන්න
|
commit_graph.select=ශාඛා තෝරන්න
|
||||||
|
@ -911,6 +912,7 @@ editor.upload_file=ගොනුව උඩුගත කරන්න
|
||||||
editor.edit_file=ගොනුව සංස්කරණය
|
editor.edit_file=ගොනුව සංස්කරණය
|
||||||
editor.preview_changes=වෙනස්කම් පෙරදසුන
|
editor.preview_changes=වෙනස්කම් පෙරදසුන
|
||||||
editor.cannot_edit_lfs_files=LFS ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
|
editor.cannot_edit_lfs_files=LFS ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
|
||||||
|
editor.cannot_edit_annex_files=Annex ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
|
||||||
editor.cannot_edit_non_text_files=ද්විමය ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
|
editor.cannot_edit_non_text_files=ද්විමය ගොනු වෙබ් අතුරු මුහුණත තුළ සංස්කරණය කළ නොහැක.
|
||||||
editor.edit_this_file=ගොනුව සංස්කරණය
|
editor.edit_this_file=ගොනුව සංස්කරණය
|
||||||
editor.this_file_locked=ගොනුවට අගුළු ලා ඇත
|
editor.this_file_locked=ගොනුවට අගුළු ලා ඇත
|
||||||
|
|
|
@ -1013,6 +1013,7 @@ view_git_blame=Zobraziť Git Blame
|
||||||
video_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'video'.
|
video_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'video'.
|
||||||
audio_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'audio'.
|
audio_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'audio'.
|
||||||
stored_lfs=Uložené pomocou Git LFS
|
stored_lfs=Uložené pomocou Git LFS
|
||||||
|
stored_annex=Uložené pomocou Git Annex
|
||||||
symbolic_link=Symbolický odkaz
|
symbolic_link=Symbolický odkaz
|
||||||
commit_graph=Graf commitov
|
commit_graph=Graf commitov
|
||||||
line=riadok
|
line=riadok
|
||||||
|
|
|
@ -907,6 +907,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".
|
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".
|
audio_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen "audio".
|
||||||
stored_lfs=Sparad med Git LFS
|
stored_lfs=Sparad med Git LFS
|
||||||
|
stored_annex=Sparad med Git Annex
|
||||||
symbolic_link=Symbolisk länk
|
symbolic_link=Symbolisk länk
|
||||||
commit_graph=Commitgraf
|
commit_graph=Commitgraf
|
||||||
commit_graph.monochrome=Mono
|
commit_graph.monochrome=Mono
|
||||||
|
@ -920,6 +921,7 @@ editor.upload_file=Ladda upp fil
|
||||||
editor.edit_file=Redigera fil
|
editor.edit_file=Redigera fil
|
||||||
editor.preview_changes=Förhandsgranska ändringar
|
editor.preview_changes=Förhandsgranska ändringar
|
||||||
editor.cannot_edit_lfs_files=LFS-filer kan inte redigeras i webbgränssnittet.
|
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.cannot_edit_non_text_files=Binära filer kan inte redigeras genom webbgränssnittet.
|
||||||
editor.edit_this_file=Redigera fil
|
editor.edit_this_file=Redigera fil
|
||||||
editor.this_file_locked=Filen är låst
|
editor.this_file_locked=Filen är låst
|
||||||
|
|
|
@ -1289,6 +1289,7 @@ view_git_blame=Git Suç Görüntüle
|
||||||
video_not_supported_in_browser=Tarayıcınız HTML5 'video' etiketini desteklemiyor.
|
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.
|
audio_not_supported_in_browser=Tarayıcınız HTML5 'audio' etiketini desteklemiyor.
|
||||||
stored_lfs=Git LFS ile depolandı
|
stored_lfs=Git LFS ile depolandı
|
||||||
|
stored_annex=Git Annex ile depolandı
|
||||||
symbolic_link=Sembolik Bağlantı
|
symbolic_link=Sembolik Bağlantı
|
||||||
executable_file=Çalıştırılabilir Dosya
|
executable_file=Çalıştırılabilir Dosya
|
||||||
commit_graph=İşleme Grafiği
|
commit_graph=İşleme Grafiği
|
||||||
|
@ -1312,6 +1313,7 @@ editor.upload_file=Dosya Yükle
|
||||||
editor.edit_file=Dosyayı Düzenle
|
editor.edit_file=Dosyayı Düzenle
|
||||||
editor.preview_changes=Değişiklikleri Önizle
|
editor.preview_changes=Değişiklikleri Önizle
|
||||||
editor.cannot_edit_lfs_files=LFS dosyaları web arayüzünde düzenlenemez.
|
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.cannot_edit_non_text_files=Bu tür dosyalar web arayüzünden düzenlenemez.
|
||||||
editor.edit_this_file=Dosyayı Düzenle
|
editor.edit_this_file=Dosyayı Düzenle
|
||||||
editor.this_file_locked=Dosya kilitlendi
|
editor.this_file_locked=Dosya kilitlendi
|
||||||
|
|
|
@ -1285,6 +1285,7 @@ file_copy_permalink=Копіювати постійне посилання
|
||||||
video_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «video».
|
video_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «video».
|
||||||
audio_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «audio».
|
audio_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 «audio».
|
||||||
stored_lfs=Збережено з Git LFS
|
stored_lfs=Збережено з Git LFS
|
||||||
|
stored_annex=Збережено з Git Annex
|
||||||
symbolic_link=Символічне посилання
|
symbolic_link=Символічне посилання
|
||||||
commit_graph=Графік комітів
|
commit_graph=Графік комітів
|
||||||
commit_graph.select=Виберіть гілки
|
commit_graph.select=Виберіть гілки
|
||||||
|
@ -1302,6 +1303,7 @@ editor.upload_file=Завантажити файл
|
||||||
editor.edit_file=Редагувати файл
|
editor.edit_file=Редагувати файл
|
||||||
editor.preview_changes=Попередній перегляд змін
|
editor.preview_changes=Попередній перегляд змін
|
||||||
editor.cannot_edit_lfs_files=Файли LFS не можна редагувати в веб-інтерфейсі.
|
editor.cannot_edit_lfs_files=Файли LFS не можна редагувати в веб-інтерфейсі.
|
||||||
|
editor.cannot_edit_annex_files=Файли Annex не можна редагувати в веб-інтерфейсі.
|
||||||
editor.cannot_edit_non_text_files=Бінарні файли не можливо редагувати у веб-інтерфейсі.
|
editor.cannot_edit_non_text_files=Бінарні файли не можливо редагувати у веб-інтерфейсі.
|
||||||
editor.edit_this_file=Редагувати файл
|
editor.edit_this_file=Редагувати файл
|
||||||
editor.this_file_locked=Файл заблоковано
|
editor.this_file_locked=Файл заблоковано
|
||||||
|
|
|
@ -1351,6 +1351,7 @@ view_git_blame=查看 Git Blame
|
||||||
video_not_supported_in_browser=您的浏览器不支持 HTML5 “video” 标签。
|
video_not_supported_in_browser=您的浏览器不支持 HTML5 “video” 标签。
|
||||||
audio_not_supported_in_browser=您的浏览器不支持 HTML5 “audio” 标签。
|
audio_not_supported_in_browser=您的浏览器不支持 HTML5 “audio” 标签。
|
||||||
stored_lfs=存储到Git LFS
|
stored_lfs=存储到Git LFS
|
||||||
|
stored_annex=存储到Git Annex
|
||||||
symbolic_link=符号链接
|
symbolic_link=符号链接
|
||||||
executable_file=可执行文件
|
executable_file=可执行文件
|
||||||
vendored = Vendored
|
vendored = Vendored
|
||||||
|
@ -1376,6 +1377,7 @@ editor.upload_file=上传文件
|
||||||
editor.edit_file=编辑文件
|
editor.edit_file=编辑文件
|
||||||
editor.preview_changes=预览变更
|
editor.preview_changes=预览变更
|
||||||
editor.cannot_edit_lfs_files=无法在 web 界面中编辑 lfs 文件。
|
editor.cannot_edit_lfs_files=无法在 web 界面中编辑 lfs 文件。
|
||||||
|
editor.cannot_edit_annex_files=无法在 web 界面中编辑 lfs 文件。
|
||||||
editor.cannot_edit_non_text_files=网页不能编辑二进制文件。
|
editor.cannot_edit_non_text_files=网页不能编辑二进制文件。
|
||||||
editor.edit_this_file=编辑文件
|
editor.edit_this_file=编辑文件
|
||||||
editor.this_file_locked=文件已锁定
|
editor.this_file_locked=文件已锁定
|
||||||
|
|
|
@ -488,6 +488,7 @@ file_view_raw=查看原始文件
|
||||||
file_permalink=永久連結
|
file_permalink=永久連結
|
||||||
|
|
||||||
stored_lfs=儲存到到 Git LFS
|
stored_lfs=儲存到到 Git LFS
|
||||||
|
stored_annex=儲存到到 Git Annex
|
||||||
|
|
||||||
editor.preview_changes=預覽更改
|
editor.preview_changes=預覽更改
|
||||||
editor.or=或
|
editor.or=或
|
||||||
|
@ -1154,4 +1155,4 @@ runners.labels = 標籤
|
||||||
|
|
||||||
[git.filemode]
|
[git.filemode]
|
||||||
|
|
||||||
[search]
|
[search]
|
||||||
|
|
|
@ -1292,6 +1292,7 @@ view_git_blame=檢視 Git Blame
|
||||||
video_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「video」標籤。
|
video_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「video」標籤。
|
||||||
audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「audio」標籤。
|
audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「audio」標籤。
|
||||||
stored_lfs=已使用 Git LFS 儲存
|
stored_lfs=已使用 Git LFS 儲存
|
||||||
|
stored_annex=已使用 Git Annex 儲存
|
||||||
symbolic_link=符號連結
|
symbolic_link=符號連結
|
||||||
commit_graph=提交線圖
|
commit_graph=提交線圖
|
||||||
commit_graph.select=選擇分支
|
commit_graph.select=選擇分支
|
||||||
|
@ -1311,6 +1312,7 @@ editor.upload_file=上傳檔案
|
||||||
editor.edit_file=編輯檔案
|
editor.edit_file=編輯檔案
|
||||||
editor.preview_changes=預覽變更
|
editor.preview_changes=預覽變更
|
||||||
editor.cannot_edit_lfs_files=無法在 web 介面中編輯 LFS 檔。
|
editor.cannot_edit_lfs_files=無法在 web 介面中編輯 LFS 檔。
|
||||||
|
editor.cannot_edit_annex_files=無法在 web 介面中編輯 Annex 檔。
|
||||||
editor.cannot_edit_non_text_files=網站介面不能編輯二進位檔案。
|
editor.cannot_edit_non_text_files=網站介面不能編輯二進位檔案。
|
||||||
editor.edit_this_file=編輯檔案
|
editor.edit_this_file=編輯檔案
|
||||||
editor.this_file_locked=檔案已被鎖定
|
editor.this_file_locked=檔案已被鎖定
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "forgejo",
|
"name": "forgejo-aneksajo",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "forgejo",
|
"name": "forgejo-aneksajo",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@citation-js/core": "0.7.14",
|
"@citation-js/core": "0.7.14",
|
||||||
"@citation-js/plugin-bibtex": "0.7.16",
|
"@citation-js/plugin-bibtex": "0.7.16",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "forgejo",
|
"name": "forgejo-aneksajo",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20.0.0"
|
"node": ">= 20.0.0"
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"forgejo.org/models"
|
"forgejo.org/models"
|
||||||
asymkey_model "forgejo.org/models/asymkey"
|
asymkey_model "forgejo.org/models/asymkey"
|
||||||
authmodel "forgejo.org/models/auth"
|
authmodel "forgejo.org/models/auth"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/cache"
|
"forgejo.org/modules/cache"
|
||||||
"forgejo.org/modules/eventsource"
|
"forgejo.org/modules/eventsource"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
|
@ -167,6 +168,8 @@ func InitWebInstalled(ctx context.Context) {
|
||||||
|
|
||||||
actions_service.Init()
|
actions_service.Init()
|
||||||
|
|
||||||
|
mustInit(annex.Init)
|
||||||
|
|
||||||
// Finally start up the cron
|
// Finally start up the cron
|
||||||
cron.NewContext(ctx)
|
cron.NewContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,12 +81,14 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||||
ownerName := ctx.Params(":owner")
|
ownerName := ctx.Params(":owner")
|
||||||
repoName := ctx.Params(":repo")
|
repoName := ctx.Params(":repo")
|
||||||
mode := perm.AccessMode(ctx.FormInt("mode"))
|
mode := perm.AccessMode(ctx.FormInt("mode"))
|
||||||
|
verbs := ctx.FormStrings("verb")
|
||||||
|
|
||||||
// Set the basic parts of the results to return
|
// Set the basic parts of the results to return
|
||||||
results := private.ServCommandResults{
|
results := private.ServCommandResults{
|
||||||
RepoName: repoName,
|
RepoName: repoName,
|
||||||
OwnerName: ownerName,
|
OwnerName: ownerName,
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
|
UserMode: perm.AccessModeNone,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now because we're not translating things properly let's just default some English strings here
|
// 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 ||
|
repo.IsPrivate ||
|
||||||
owner.Visibility.IsPrivate() ||
|
owner.Visibility.IsPrivate() ||
|
||||||
(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
|
(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) {
|
setting.Service.RequireSignInView) {
|
||||||
if key.Type == asymkey_model.KeyTypeDeploy {
|
if key.Type == asymkey_model.KeyTypeDeploy {
|
||||||
|
results.UserMode = deployKey.Mode
|
||||||
if deployKey.Mode < mode {
|
if deployKey.Mode < mode {
|
||||||
ctx.JSON(http.StatusUnauthorized, private.Response{
|
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),
|
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
|
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())
|
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{
|
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),
|
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
|
return
|
||||||
}
|
}
|
||||||
|
results.UserMode = perm.AccessModeWrite
|
||||||
results.RepoID = repo.ID
|
results.RepoID = repo.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,13 +386,14 @@ func ServCommand(ctx *context.PrivateContext) {
|
||||||
return
|
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.IsWiki,
|
||||||
results.DeployKeyID,
|
results.DeployKeyID,
|
||||||
results.KeyID,
|
results.KeyID,
|
||||||
results.KeyName,
|
results.KeyName,
|
||||||
results.UserName,
|
results.UserName,
|
||||||
results.UserID,
|
results.UserID,
|
||||||
|
results.UserMode,
|
||||||
results.OwnerName,
|
results.OwnerName,
|
||||||
results.RepoName,
|
results.RepoName,
|
||||||
results.RepoID)
|
results.RepoID)
|
||||||
|
|
153
routers/web/repo/annex.go
Normal file
153
routers/web/repo/annex.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forgejo.org/models/perm"
|
||||||
|
access_model "forgejo.org/models/perm/access"
|
||||||
|
repo_model "forgejo.org/models/repo"
|
||||||
|
"forgejo.org/models/unit"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
|
"forgejo.org/modules/graceful"
|
||||||
|
"forgejo.org/modules/log"
|
||||||
|
"forgejo.org/modules/setting"
|
||||||
|
services_context "forgejo.org/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.Env = append(os.Environ(),
|
||||||
|
"GIT_AUTHOR_NAME="+setting.AppName,
|
||||||
|
"GIT_AUTHOR_EMAIL="+setting.RunUser+"@"+setting.Domain,
|
||||||
|
"GIT_COMMITTER_NAME="+setting.AppName,
|
||||||
|
"GIT_COMMITTER_EMAIL="+setting.RunUser+"@"+setting.Domain,
|
||||||
|
)
|
||||||
|
_ = 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 "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
"forgejo.org/models/unit"
|
"forgejo.org/models/unit"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/base"
|
"forgejo.org/modules/base"
|
||||||
"forgejo.org/modules/charset"
|
"forgejo.org/modules/charset"
|
||||||
csv_module "forgejo.org/modules/csv"
|
csv_module "forgejo.org/modules/csv"
|
||||||
|
@ -72,7 +73,21 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner
|
||||||
return st
|
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 {
|
if err != nil {
|
||||||
log.Error("GuessContentType failed: %v", err)
|
log.Error("GuessContentType failed: %v", err)
|
||||||
return st
|
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())
|
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
|
// MediaCommitURL creates a relative URL for the commit media (plain git, LFS, or annex content) in the given repository
|
||||||
func RawCommitURL(owner, name string, commit *git.Commit) string {
|
func MediaCommitURL(owner, name string, commit *git.Commit) string {
|
||||||
return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/raw/commit/" + url.PathEscape(commit.ID.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
|
// setPathsCompareContext sets context data for source and raw paths
|
||||||
func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
|
func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
|
||||||
ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head)
|
ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head)
|
||||||
ctx.Data["RawPath"] = RawCommitURL(headOwner, headName, head)
|
ctx.Data["RawPath"] = MediaCommitURL(headOwner, headName, head)
|
||||||
if base != nil {
|
if base != nil {
|
||||||
ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, base)
|
ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, base)
|
||||||
ctx.Data["BeforeRawPath"] = RawCommitURL(headOwner, headName, base)
|
ctx.Data["BeforeRawPath"] = MediaCommitURL(headOwner, headName, base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
git_model "forgejo.org/models/git"
|
git_model "forgejo.org/models/git"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/httpcache"
|
"forgejo.org/modules/httpcache"
|
||||||
"forgejo.org/modules/lfs"
|
"forgejo.org/modules/lfs"
|
||||||
|
@ -78,6 +79,26 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
|
||||||
}
|
}
|
||||||
closed = true
|
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)
|
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
gocontext "context"
|
gocontext "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -545,6 +546,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
|
// GetTextFile implements Git dumb HTTP
|
||||||
func GetTextFile(p string) func(*context.Context) {
|
func GetTextFile(p string) func(*context.Context) {
|
||||||
return func(ctx *context.Context) {
|
return func(ctx *context.Context) {
|
||||||
|
@ -597,3 +634,34 @@ func GetIdxFile(ctx *context.Context) {
|
||||||
h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
|
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 "forgejo.org/models/unit"
|
unit_model "forgejo.org/models/unit"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
"forgejo.org/modules/actions"
|
"forgejo.org/modules/actions"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/base"
|
"forgejo.org/modules/base"
|
||||||
"forgejo.org/modules/charset"
|
"forgejo.org/modules/charset"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
|
@ -209,14 +210,59 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileInfo struct {
|
type fileInfo struct {
|
||||||
isTextFile bool
|
isTextFile bool
|
||||||
isLFSFile bool
|
isLFSFile bool
|
||||||
fileSize int64
|
isAnnexFile bool
|
||||||
lfsMeta *lfs.Pointer
|
isAnnexFilePresent bool
|
||||||
st typesniffer.SniffedType
|
fileSize int64
|
||||||
|
lfsMeta *lfs.Pointer
|
||||||
|
st typesniffer.SniffedType
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
|
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()
|
dataRc, err := blob.DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
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?
|
// FIXME: what happens when README file is an image?
|
||||||
if !isTextFile || !setting.LFS.StartServer {
|
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)
|
pointer, _ := lfs.ReadPointerFromBuffer(buf)
|
||||||
if !pointer.IsValid() { // fallback to plain file
|
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)
|
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
||||||
if err != nil { // fallback to plain file
|
if err != nil { // fallback to plain file
|
||||||
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
|
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()
|
dataRc.Close()
|
||||||
|
@ -262,7 +308,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
|
||||||
|
|
||||||
st = typesniffer.DetectContentType(buf)
|
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) {
|
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),
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
Blob: target.Blob(),
|
||||||
}, rd)
|
}, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
||||||
|
@ -451,10 +498,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
isDisplayingSource := ctx.FormString("display") == "source"
|
isDisplayingSource := ctx.FormString("display") == "source"
|
||||||
isDisplayingRendered := !isDisplayingSource
|
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)
|
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()
|
isRepresentableAsText := fInfo.st.IsRepresentableAsText()
|
||||||
if !isRepresentableAsText {
|
if !isRepresentableAsText {
|
||||||
// If we can't show plain text, always try to render.
|
// If we can't show plain text, always try to render.
|
||||||
|
@ -462,6 +516,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
isDisplayingRendered = true
|
isDisplayingRendered = true
|
||||||
}
|
}
|
||||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
||||||
|
ctx.Data["IsAnnexFile"] = fInfo.isAnnexFile
|
||||||
|
ctx.Data["IsAnnexFilePresent"] = fInfo.isAnnexFilePresent
|
||||||
ctx.Data["FileSize"] = fInfo.fileSize
|
ctx.Data["FileSize"] = fInfo.fileSize
|
||||||
ctx.Data["IsTextFile"] = fInfo.isTextFile
|
ctx.Data["IsTextFile"] = fInfo.isTextFile
|
||||||
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
|
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
|
||||||
|
@ -496,6 +552,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
// Assume file is not editable first.
|
// Assume file is not editable first.
|
||||||
if fInfo.isLFSFile {
|
if fInfo.isLFSFile {
|
||||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
|
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 {
|
} else if !isRepresentableAsText {
|
||||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
|
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
|
||||||
}
|
}
|
||||||
|
@ -550,6 +608,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
},
|
},
|
||||||
Metas: metas,
|
Metas: metas,
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
Blob: entry.Blob(),
|
||||||
}, rd)
|
}, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
|
@ -603,7 +662,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
ctx.Data["FileContent"] = fileContent
|
ctx.Data["FileContent"] = fileContent
|
||||||
ctx.Data["LineEscapeStatus"] = statuses
|
ctx.Data["LineEscapeStatus"] = statuses
|
||||||
}
|
}
|
||||||
if !fInfo.isLFSFile {
|
if !fInfo.isLFSFile && !fInfo.isAnnexFile {
|
||||||
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
||||||
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
|
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
|
||||||
ctx.Data["CanEditFile"] = false
|
ctx.Data["CanEditFile"] = false
|
||||||
|
@ -648,6 +707,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||||
},
|
},
|
||||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(ctx),
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
Blob: entry.Blob(),
|
||||||
}, rd)
|
}, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
|
@ -1171,6 +1231,15 @@ PostRecentBranchCheck:
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["CodeSearchOptions"] = git.GrepSearchOptions
|
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)
|
ctx.HTML(http.StatusOK, tplRepoHome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -350,6 +350,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) {
|
federationEnabled := func(ctx *context.Context) {
|
||||||
if !setting.Federation.Enabled {
|
if !setting.Federation.Enabled {
|
||||||
ctx.Error(http.StatusNotFound)
|
ctx.Error(http.StatusNotFound)
|
||||||
|
@ -952,6 +966,9 @@ func registerRoutes(m *web.Route) {
|
||||||
// ***** END: Organization *****
|
// ***** END: Organization *****
|
||||||
|
|
||||||
// ***** START: Repository *****
|
// ***** 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.Group("/repo", func() {
|
||||||
m.Get("/create", repo.Create)
|
m.Get("/create", repo.Create)
|
||||||
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
|
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
|
||||||
|
@ -1632,6 +1649,12 @@ func registerRoutes(m *web.Route) {
|
||||||
})
|
})
|
||||||
}, ignSignInAndCsrf, lfsServerEnabled)
|
}, 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)
|
gitHTTPRouters(m)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -61,6 +61,17 @@ func isArchivePath(req *http.Request) bool {
|
||||||
return archivePathRe.MatchString(req.URL.Path)
|
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
|
// 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) {
|
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
|
||||||
// We need to regenerate the session...
|
// We need to regenerate the session...
|
||||||
|
|
|
@ -43,8 +43,8 @@ func (b *Basic) Name() string {
|
||||||
// name/token on successful validation.
|
// name/token on successful validation.
|
||||||
// Returns nil if header is empty or validation fails.
|
// 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) {
|
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
|
// 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) {
|
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) && !isAnnexPath(req) {
|
||||||
return nil, nil
|
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"
|
||||||
|
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
|
"forgejo.org/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 "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
"forgejo.org/models/unit"
|
"forgejo.org/models/unit"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/cache"
|
"forgejo.org/modules/cache"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/log"
|
"forgejo.org/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
|
var headUser *user_model.User
|
||||||
err = pr.HeadRepo.LoadOwner(ctx)
|
err = pr.HeadRepo.LoadOwner(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -202,6 +202,26 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat
|
||||||
return nil
|
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
|
// WriteTree writes the current index as a tree to the object db and returns its hash
|
||||||
func (t *TemporaryUploadRepository) WriteTree() (string, error) {
|
func (t *TemporaryUploadRepository) WriteTree() (string, error) {
|
||||||
stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})
|
stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})
|
||||||
|
|
|
@ -6,13 +6,16 @@ package files
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
git_model "forgejo.org/models/git"
|
git_model "forgejo.org/models/git"
|
||||||
repo_model "forgejo.org/models/repo"
|
repo_model "forgejo.org/models/repo"
|
||||||
user_model "forgejo.org/models/user"
|
user_model "forgejo.org/models/user"
|
||||||
|
"forgejo.org/modules/annex"
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/lfs"
|
"forgejo.org/modules/lfs"
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
|
@ -89,7 +92,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||||
defer t.Close()
|
defer t.Close()
|
||||||
|
|
||||||
hasOldBranch := true
|
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 {
|
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -105,10 +108,30 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy uploaded files into repository.
|
r, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||||
if err := copyUploadedLFSFilesIntoRepository(infos, t, opts.TreePath); err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// Now write the tree
|
||||||
treeHash, err := t.WriteTree()
|
treeHash, err := t.WriteTree()
|
||||||
|
@ -246,3 +269,57 @@ func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) er
|
||||||
}
|
}
|
||||||
return nil
|
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}}
|
{{if .FileSize}}
|
||||||
<div class="file-info-entry">
|
<div class="file-info-entry">
|
||||||
{{ctx.Locale.TrSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
|
{{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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .LFSLock}}
|
{{if .LFSLock}}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
api "forgejo.org/modules/structs"
|
api "forgejo.org/modules/structs"
|
||||||
"forgejo.org/services/forms"
|
"forgejo.org/services/forms"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -460,3 +461,27 @@ func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, r
|
||||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
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)
|
"ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// reset ssh wrapper afterwards
|
||||||
|
_gitSSH, gitSSHExists := os.LookupEnv("GIT_SSH")
|
||||||
|
defer func() {
|
||||||
|
if gitSSHExists {
|
||||||
|
t.Setenv("GIT_SSH", _gitSSH)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_gitSSHCommand, gitSSHCommandExists := os.LookupEnv("GIT_SSH_COMMAND")
|
||||||
|
defer func() {
|
||||||
|
if gitSSHCommandExists {
|
||||||
|
t.Setenv("GIT_SSH_COMMAND", _gitSSHCommand)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_gitSSHVariant, gitSSHVariantExists := os.LookupEnv("GIT_SSH_VARIANT")
|
||||||
|
defer func() {
|
||||||
|
if gitSSHVariantExists {
|
||||||
|
t.Setenv("GIT_SSH_VARIANT", _gitSSHVariant)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Setup ssh wrapper
|
// Setup ssh wrapper
|
||||||
t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
|
t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
|
||||||
t.Setenv("GIT_SSH_COMMAND",
|
t.Setenv("GIT_SSH_COMMAND",
|
||||||
|
@ -51,6 +73,13 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
|
||||||
callback(keyFile)
|
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 {
|
func createSSHUrl(gitPath string, u *url.URL) *url.URL {
|
||||||
u2 := *u
|
u2 := *u
|
||||||
u2.Scheme = "ssh"
|
u2.Scheme = "ssh"
|
||||||
|
|
|
@ -97,6 +97,9 @@ DISABLE_QUERY_AUTH_TOKEN = true
|
||||||
[lfs]
|
[lfs]
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
|
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
|
||||||
|
|
||||||
|
[annex]
|
||||||
|
ENABLED = true
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,9 @@ MINIO_LOCATION = us-east-1
|
||||||
MINIO_USE_SSL = false
|
MINIO_USE_SSL = false
|
||||||
MINIO_CHECKSUM_ALGORITHM = md5
|
MINIO_CHECKSUM_ALGORITHM = md5
|
||||||
|
|
||||||
|
[annex]
|
||||||
|
ENABLED = true
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,9 @@ JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
|
||||||
[lfs]
|
[lfs]
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
|
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
|
||||||
|
|
||||||
|
[annex]
|
||||||
|
ENABLED = true
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -483,3 +485,80 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
|
||||||
|
|
||||||
return CreateDeclarativeRepoWithOptions(t, owner, opts)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -246,6 +246,7 @@ td .commit-summary {
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.file.list #repo-files-table tbody .svg.octicon-file,
|
.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-symlink-file,
|
||||||
.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-symlink {
|
.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-symlink {
|
||||||
color: var(--color-secondary-dark-7);
|
color: var(--color-secondary-dark-7);
|
||||||
|
|
|
@ -92,7 +92,17 @@ export function initImageDiff() {
|
||||||
return loadElem(img, info.path);
|
return loadElem(img, info.path);
|
||||||
}));
|
}));
|
||||||
// only the first images is associated with $boundsInfo
|
// 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') {
|
if (info.mime === 'image/svg+xml') {
|
||||||
const resp = await GET(info.path);
|
const resp = await GET(info.path);
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue