Compare commits

...

22 Commits

Author SHA1 Message Date
init
364a8fad8f add open with vscode feature 2025-09-29 20:13:44 +08:00
init
d0f9cd5ef0 fix bug 2025-09-19 15:56:31 +08:00
init
5caf41cf28 fix bug 2025-09-19 12:50:54 +08:00
init
2e7927a23e fix bug 2025-09-19 10:36:38 +08:00
init
79ace0ac38 fix bug 2025-09-18 20:14:43 +08:00
init
6e9f6a829d fix bug 2025-09-18 19:29:29 +08:00
init
83085dc2d0 fix bug 2025-09-18 19:28:56 +08:00
init
a039f17913 fix bug 2025-09-18 19:28:56 +08:00
init
895b10cc2f fix bug 2025-09-18 19:28:56 +08:00
init
fd532af745 fix bug 2025-09-18 19:26:53 +08:00
init
6ff9b9c665 fix bug 2025-09-18 19:25:17 +08:00
init
7bf829b599 fix bug 2025-09-18 19:19:57 +08:00
init
a19272de73 add configuration features 2025-09-18 19:15:35 +08:00
孟宁
51820bf0eb Merge branch 'main' into mergeDevContainer 2025-09-13 13:47:28 +08:00
init
6686a44316 add webterminal dockerfile 2025-09-04 10:50:44 +08:00
孟宁
7f86efe563 默认Enable DevContainer 2025-08-31 10:25:43 +08:00
孟宁
de6fd128cf Merge branch 'main' into mergeDevContainer 2025-08-30 14:27:08 +08:00
init
41955ed427 change devcontainer button of install page 2025-08-29 16:26:49 +08:00
init
0e9b1020d9 auto start webterminal 2025-08-29 16:08:02 +08:00
init
923816b0b9 IDE connect feature 2025-08-21 21:31:51 +08:00
init
db9f69958b add devcontainer feature 2025-08-16 18:31:14 +08:00
init
52b2fce7b0 mergeDevContainer 2025-08-11 16:32:10 +08:00
repo.diff.stats_desc%!(EXTRA int=43, int=5903, int=43)

repo.diff.view_file

@@ -918,9 +918,11 @@ generate-manpage: ## generate manpage
.PHONY: devstar
devstar:
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
.PHONY: docker
docker:
docker build -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal .
docker build --disable-content-trust=false -t $(DOCKER_REF) .
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .

repo.diff.view_file

@@ -73,7 +73,7 @@ RUN chown git:git /var/lib/gitea /etc/gitea
COPY --from=build-env /tmp/local /
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/webTerminal.sh /app/gitea/webTerminal.sh
# git:git
USER 1000:1000
ENV GITEA_WORK_DIR=/var/lib/gitea

repo.diff.view_file

@@ -0,0 +1,40 @@
FROM docker.io/library/ubuntu:24.04 AS build-env
RUN apt-get update && \
apt-get install -y \
git \
build-essential \
cmake \
libjson-c-dev \
libwebsockets-dev
RUN git clone https://devstar.cn/devstar/webTerminal.git /home/webTerminal
# 设置工作目录并构建
WORKDIR /home/webTerminal/build
RUN cmake ..
RUN make && make install
FROM ubuntu:24.04
# 从构建阶段复制编译好的程序
COPY --from=build-env /home/webTerminal/build/ttyd /home/webTerminal/build/ttyd
# 只安装运行时需要的库
RUN apt-get update && \
apt-get install -y \
curl && \
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && \
apt-get install -y tini \
libjson-c-dev \
libwebsockets-dev && \
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null && \
apt-get update && apt-get install -y docker-ce-cli && \
apt remove --purge curl -y && apt autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/home/webTerminal/build/ttyd", "-W", "bash"]

7
go.mod
repo.diff.view_file

@@ -128,6 +128,7 @@ require (
google.golang.org/protobuf v1.36.6
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.3
mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.13
@@ -152,7 +153,6 @@ require (
golang.org/x/term v0.32.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/apimachinery v0.33.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
@@ -167,9 +167,9 @@ require (
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/ArtisanCloud/PowerLibs/v3 v3.3.2 // indirect
github.com/ArtisanCloud/PowerLibs/v3 v3.3.2
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.8 // indirect
github.com/ArtisanCloud/PowerWeChat/v3 v3.4.21 // indirect
github.com/ArtisanCloud/PowerWeChat/v3 v3.4.21
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -301,7 +301,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect

24
go.sum
repo.diff.view_file

@@ -337,6 +337,8 @@ github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0g
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
@@ -345,6 +347,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
@@ -355,7 +359,10 @@ github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkv
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
@@ -611,11 +618,13 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -698,6 +707,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
@@ -706,6 +717,7 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -782,8 +794,14 @@ gitlab.com/gitlab-org/api/client-go v0.127.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAj
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -812,8 +830,6 @@ golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ug
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=

repo.diff.view_file

@@ -0,0 +1,35 @@
package devcontainer
import (
"code.gitea.io/gitea/models/db"
)
type Devcontainer struct {
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键devContainerId')"`
Name string `xorm:"VARCHAR(64) charset=utf8mb4 collate=utf8mb4_bin UNIQUE NOT NULL 'name' comment('devContainer名称自动生成')"`
DevcontainerHost string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_host' comment('SSH Host')"`
DevcontainerPort uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_port' comment('SSH Port')"`
DevcontainerStatus uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_status' comment('SSH Status')"`
DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"`
DevcontainerWorkDir string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_work_dir' comment('SSH 工作路径,典型值 ~/${project_name}256字节以内')"`
RepoId int64 `xorm:"BIGINT NOT NULL FK('repo_id') REFERENCES repository(id) ON DELETE CASCADE 'repo_id' comment('repository表主键')"`
UserId int64 `xorm:"BIGINT NOT NULL FK('user_id') REFERENCES user(id) ON DELETE CASCADE 'user_id' comment('user表主键')"`
CreatedUnix int64 `xorm:"BIGINT 'created_unix' comment('创建时间戳')"`
UpdatedUnix int64 `xorm:"BIGINT 'updated_unix' comment('更新时间戳')"`
}
type DevcontainerOutput struct {
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键devContainerId')"`
RepoId int64 `xorm:"BIGINT NOT NULL unique(uniquename) 'repo_id' comment('repository表主键')"`
UserId int64 `xorm:"BIGINT NOT NULL unique(uniquename) 'user_id' comment('user表主键')"`
Status string `xorm:"VARCHAR(255) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'status' comment('status')"`
Output string `xorm:"TEXT 'output' comment('output')"`
Command string `xorm:"TEXT 'command' comment('command')"`
ListId int64 `xorm:"BIGINT NOT NULL unique(uniquename) 'list_id' comment('list_id')"`
DevcontainerId int64 `xorm:"BIGINT NOT NULL FK('devcontainer_id') REFERENCES devcontainer(id) ON DELETE CASCADE 'devcontainer_id' comment('devcontainer表主键')"`
}
func init() {
db.RegisterModel(new(Devcontainer))
db.RegisterModel(new(DevcontainerOutput))
}

repo.diff.view_file

@@ -14,10 +14,7 @@ func AddDBWeChatUser(x *xorm.Engine) error {
// 创建数据库表格
err := x.Sync(new(wechat_models.UserWechatOpenid))
if err != nil {
return ErrMigrateDevstarDatabase{
Step: "create table 'user_wechat_openid'",
Message: err.Error(),
}
return err
}
return nil

repo.diff.view_file

@@ -124,6 +124,7 @@ type User struct {
AllowGitHook bool
AllowImportLocal bool // Allow migrate repository by local path
AllowCreateOrganization bool `xorm:"DEFAULT true"`
AllowCreateDevcontainer bool `xorm:"DEFAULT false"`
// true: the user is not allowed to log in Web UI. Git/SSH access could still be allowed (please refer to Git/SSH access related code/documents)
ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"`
@@ -268,6 +269,11 @@ func (u *User) CanCreateOrganization() bool {
return u.IsAdmin || (u.AllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation)
}
// CanCreateDevcontianer returns true if user can create organisation.
func (u *User) CanCreateDevcontainer() bool {
return u.AllowCreateDevcontainer
}
// CanEditGitHook returns true if user can edit Git hooks.
func (u *User) CanEditGitHook() bool {
return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)
@@ -633,6 +639,7 @@ type CreateUserOverwriteOptions struct {
KeepEmailPrivate optional.Option[bool]
Visibility *structs.VisibleType
AllowCreateOrganization optional.Option[bool]
AllowCreateDevcontainer optional.Option[bool]
EmailNotificationsPreference *string
MaxRepoCreation *int
Theme *string

repo.diff.view_file

@@ -58,6 +58,7 @@ func NewActionsUser() *User {
LoginName: ActionsUserName,
Type: UserTypeBot,
AllowCreateOrganization: true,
AllowCreateDevcontainer: false,
Visibility: structs.VisibleTypePublic,
}
}

repo.diff.view_file

@@ -2,25 +2,58 @@ package docker
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
)
// CreateDockerClient 创建Docker客户端
func CreateDockerClient(ctx context.Context) (*client.Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
log.Info("检查 Docker 环境")
// 1. 检查 Docker 环境
dockerSocketPath, err := GetDockerSocketPath()
if err != nil {
return nil, err
}
log.Info("dockerSocketPath: %s", dockerSocketPath)
// 2. 创建docker client 并且检查是否运行
opts := []client.Opt{
client.FromEnv,
}
if dockerSocketPath != "" {
opts = append(opts, client.WithHost(dockerSocketPath))
}
cli, err := client.NewClientWithOpts(opts...)
if err != nil {
return nil, err
}
_, err = cli.Ping(ctx)
if err != nil {
return nil, fmt.Errorf("docker未运行, %w", err)
}
return cli, nil
}
// GetDockerSocketPath 获取Docker Socket路径
func GetDockerSocketPath() (string, error) {
// 检查环境变量
socket, found := os.LookupEnv("DOCKER_HOST")
if found {
return socket, nil
}
// 检查常见的Docker socket路径
socketPaths := []string{
"/var/run/docker.sock",
@@ -31,20 +64,55 @@ func GetDockerSocketPath() (string, error) {
`\\.\pipe\docker_engine`,
"$HOME/.docker/run/docker.sock",
}
for _, path := range socketPaths {
if _, err := os.Stat(path); err == nil {
return path, nil
// 测试Docker默认路径
for _, p := range socketPaths {
if _, err := os.Lstat(os.ExpandEnv(p)); err == nil {
if strings.HasPrefix(p, `\\.\`) {
return "npipe://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
return "unix://" + filepath.ToSlash(os.ExpandEnv(p)), nil
}
}
// 找不到
return "", fmt.Errorf("Docker未安装")
}
func GetContainerID(cli *client.Client, containerName string) (string, error) {
containerJSON, err := cli.ContainerInspect(context.Background(), containerName)
if err != nil {
return "", err
}
return containerJSON.ID, nil
}
// 如果找不到,返回默认路径
return "/var/run/docker.sock", nil
func GetContainerStatus(cli *client.Client, containerID string) (string, error) {
containerInfo, err := cli.ContainerInspect(context.Background(), containerID)
if err != nil {
return "", err
}
state := containerInfo.State
return state.Status, nil
}
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
cmd := exec.Command("sh", "-c", script)
_, err := cmd.CombinedOutput()
if err != nil {
return err
}
// 推送到仓库
script = "docker " + "-H " + dockerHost + " push " + imageRef
cmd = exec.Command("sh", "-c", script)
_, err = cmd.CombinedOutput()
if err != nil {
return err
}
return nil
}
// PullImage 拉取Docker镜像
func PullImage(cli *client.Client, dockerHost, imageName string) error {
ctx := context.Background()
func PullImage(ctx context.Context, cli *client.Client, dockerHost, imageName string) error {
reader, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
if err != nil {
@@ -58,27 +126,26 @@ func PullImage(cli *client.Client, dockerHost, imageName string) error {
}
// CreateAndStartContainer 创建并启动容器
func CreateAndStartContainer(cli *client.Client, imageName string, cmd []string, env []string, binds []string, ports map[string]string, containerName string) error {
ctx := context.Background()
func CreateAndStartContainer(ctx context.Context, cli *client.Client, imageName string, cmd []string, env []string, binds []string, ports nat.PortSet, containerName string) error {
// 配置容器
config := &container.Config{
Image: imageName,
Env: env,
}
if ports != nil {
config.ExposedPorts = ports
}
if cmd != nil {
config.Cmd = cmd
}
hostConfig := &container.HostConfig{
Binds: binds,
}
// 如果有端口映射配置
if ports != nil && len(ports) > 0 {
// 这里可以根据需要添加端口映射逻辑
// hostConfig.PortBindings = portBindings
RestartPolicy: container.RestartPolicy{
Name: "always", // 设置为 always
},
PublishAllPorts: true,
}
// 创建容器
@@ -92,8 +159,7 @@ func CreateAndStartContainer(cli *client.Client, imageName string, cmd []string,
}
// DeleteContainer 停止并删除指定名称的容器
func DeleteContainer(cli *client.Client, containerName string) error {
ctx := context.Background()
func DeleteContainer(ctx context.Context, cli *client.Client, containerName string) error {
// 首先尝试停止容器
timeout := 10
@@ -115,3 +181,86 @@ func DeleteContainer(cli *client.Client, containerName string) error {
return nil
}
func IsContainerNotFound(err error) bool {
if client.IsErrNotFound(err) {
return true
}
return false
}
// 获取容器端口映射到了主机哪个端口,参数: DockerClient、containerName、容器端口号
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
// 创建 Docker 客户端
cli, err := CreateDockerClient(ctx)
if err != nil {
return 0, err
}
// 获取容器 ID
containerID, err := GetContainerID(cli, containerName)
if err != nil {
return 0, err
}
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), containerID)
if err != nil {
return 0, err
}
// 获取端口映射信息
portBindings := containerJSON.NetworkSettings.Ports
for containerPort, bindings := range portBindings {
for _, binding := range bindings {
log.Info("容器端口 %s 映射到主机 %s 端口 %s \n", containerPort, binding.HostIP, binding.HostPort)
if containerPort.Port() == port {
port_64, err := strconv.ParseUint(binding.HostPort, 10, 16)
if err != nil {
return 0, err
}
return uint16(port_64), nil
}
}
}
return 0, fmt.Errorf("容器未开放端口 %s", port)
}
func ExecCommandInContainer(ctx context.Context, cli *client.Client, containerName string, command string) (string, error) {
containerID, err := GetContainerID(cli, containerName)
if err != nil {
log.Info("创建执行实例失败", err)
return "", err
}
cmdList := []string{"sh", "-c", command}
execConfig := types.ExecConfig{
Cmd: cmdList,
AttachStdout: true,
AttachStderr: true,
}
// 创建执行实例
exec, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
if err != nil {
log.Info("创建执行实例失败", err)
return "", err
}
// 附加到执行实例
resp, err := cli.ContainerExecAttach(context.Background(), exec.ID, types.ExecStartCheck{})
if err != nil {
log.Info("命令附加到执行实例失败", err)
return "", err
}
defer resp.Close()
// 启动执行实例
err = cli.ContainerExecStart(context.Background(), exec.ID, types.ExecStartCheck{})
if err != nil {
log.Info("启动执行实例失败", err)
return "", err
}
// 自定义缓冲区
var outBuf, errBuf strings.Builder
// 读取输出
_, err = stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
if err != nil {
log.Info("Error reading output for command %v: %v\n", command, err)
return "", err
}
return outBuf.String() + errBuf.String(), nil
}

repo.diff.view_file

@@ -0,0 +1,14 @@
package setting
var DevContainerConfig = struct {
Enable bool
Web_Terminal_Image string
Web_Terminal_Container string
}{}
func loadDevContainerSettingsFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("devcontainer")
DevContainerConfig.Enable = sec.Key("ENABLE").MustBool(true)
DevContainerConfig.Web_Terminal_Image = sec.Key("WEB_TERMINAL_IMAGE").MustString("devstar.cn/devstar/webterminal:latest")
DevContainerConfig.Web_Terminal_Container = sec.Key("WEB_TERMINAL_CONTAINER").MustString("")
}

repo.diff.view_file

@@ -219,6 +219,7 @@ func LoadSettings() {
loadFederationFrom(CfgProvider)
loadRunnerSettingsFrom(CfgProvider)
loadK8sSettingsFrom(CfgProvider)
loadDevContainerSettingsFrom(CfgProvider)
loadWechatSettingsFrom(CfgProvider)
}
@@ -229,6 +230,7 @@ func LoadSettingsForInstall() {
loadMailerFrom(CfgProvider)
loadRunnerSettingsFrom(CfgProvider)
loadK8sSettingsFrom(CfgProvider)
loadDevContainerSettingsFrom(CfgProvider)
loadWechatSettingsFrom(CfgProvider)
}

repo.diff.view_file

@@ -54,6 +54,7 @@ type EditUserOption struct {
MaxRepoCreation *int `json:"max_repo_creation"`
ProhibitLogin *bool `json:"prohibit_login"`
AllowCreateOrganization *bool `json:"allow_create_organization"`
AllowCreateDevcontainer *bool `json:"allow_create_devcontainer"`
Restricted *bool `json:"restricted"`
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
}

repo.diff.view_file

@@ -303,6 +303,9 @@ log_root_path = Log Path
log_root_path_helper = Log files will be written to this directory.
optional_title = Optional Settings
devcontainer_title = DevContainer Settings
devcontainer_enable = Enable DevContainer
web_terminal_failed = Failed to start web terminal: %v
k8s_title = Kubernetes Settings
k8s_enable = Enable Kubernetes
k8s_url = Kubernetes API URL
@@ -3152,6 +3155,7 @@ users.allow_git_hook = May Create Git Hooks
users.allow_git_hook_tooltip = Git Hooks are executed as the OS user running Gitea and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all Gitea repositories as well as the database used by Gitea. Consequently they are also able to gain Gitea administrator privileges.
users.allow_import_local = May Import Local Repositories
users.allow_create_organization = May Create Organizations
users.allow_create_devcontainer= May Create Devcontainers
users.update_profile = Update User Account
users.delete_account = Delete User Account
users.cannot_delete_self = "You cannot delete yourself"

repo.diff.view_file

@@ -298,6 +298,9 @@ log_root_path=日志路径
log_root_path_helper=日志文件将写入此目录。
optional_title=可选设置
devcontainer_title = DevContainer设置
devcontainer_enable = 启用 DevContainer
web_terminal_failed = 启动 WebTerminal 失败: %v
k8s_title = Kubernetes设置
k8s_enable = 启用 Kubernetes
k8s_url = Kubernetes API 地址
@@ -3141,6 +3144,7 @@ users.allow_git_hook=允许创建 Git 钩子
users.allow_git_hook_tooltip=Git 钩子将会以操作系统用户运行,拥有同样的主机访问权限。因此,拥有此特殊的 Git 钩子权限将能够访问合修改所有的 Gitea 仓库或者 Gitea 的数据库。同时也能获得 Gitea 的管理员权限。
users.allow_import_local=允许导入本地仓库
users.allow_create_organization=允许创建组织
users.allow_create_devcontainer=允许创建开发容器
users.update_profile=更新帐户
users.delete_account=删除帐户
users.cannot_delete_self=您不能删除自己

repo.diff.view_file

@@ -0,0 +1,224 @@
package devcontainer
import (
"strconv"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
web_module "code.gitea.io/gitea/modules/web"
Result "code.gitea.io/gitea/routers/entity"
context "code.gitea.io/gitea/services/context"
gitea_web_context "code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
"code.gitea.io/gitea/services/forms"
)
// CreateRepoDevcontainer 创建 某用户在某仓库的 DevContainer
//
// POST /api/devcontainer
// 请求体参数:
// -- repoId: 需要为哪个仓库创建 DevContainer
// -- sshPublicKeyList: 列表填入用户希望临时使用的SSH会话加密公钥
// 注意:必须携带 用户登录凭证
func CreateRepoDevcontainer(ctx *context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
if ctx == nil || ctx.Doer == nil {
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 2. 检查表单校验规则是否失败
if ctx.HasError() {
// POST Binding 表单正则表达式校验失败,返回 API 错误信息
failedToValidateFormData := &Result.ResultType{
Code: Result.RespFailedIllegalParams.Code,
Msg: Result.RespFailedIllegalParams.Msg,
Data: map[string]string{
"ErrorMsg": ctx.GetErrMsg(),
},
}
failedToValidateFormData.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 3. 解析 repoId
form := web_module.GetForm(ctx).(*forms.CreateRepoDevcontainerForm)
repoId, err := strconv.ParseInt(form.RepoId, 10, 64)
if err != nil || repoId <= 0 {
failedToParseRepoId := Result.ResultType{
Code: Result.RespFailedIllegalParams.Code,
Msg: Result.RespFailedIllegalParams.Msg,
Data: map[string]string{
// fix nullptr dereference of `err.Error()` when repoId == 0
"ErrorMsg": "repoId 必须是正数",
},
}
failedToParseRepoId.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 4. 调用 API Service 层创建 DevContainer
repo, err := repo.GetRepositoryByID(ctx, repoId)
if err != nil {
errCreateDevcontainer := Result.ResultType{
Code: Result.RespFailedCreateDevcontainer.Code,
Msg: Result.RespFailedCreateDevcontainer.Msg,
Data: map[string]string{
"ErrorMsg": "repo not found",
},
}
errCreateDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
devcontainer_service.CreateDevcontainerConfiguration(repo, ctx.Doer)
err = devcontainer_service.CreateDevcontainerAPIService(ctx, repo, ctx.Doer, form.SSHPublicKeyList, false)
if err != nil {
errCreateDevcontainer := Result.ResultType{
Code: Result.RespFailedCreateDevcontainer.Code,
Msg: Result.RespFailedCreateDevcontainer.Msg,
Data: map[string]string{
"ErrorMsg": err.Error(),
},
}
errCreateDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 4. 创建 DevContainer 成功,直接返回
Result.RespSuccess.RespondJson2HttpResponseWriter(ctx.Resp)
}
// GetDevcontainer 查找某用户在某仓库的 DevContainer
//
// GET /api/devcontainer
// 请求体参数:
// -- repoId: 需要为哪个仓库创建 DevContainer
// -- wait: 是否等待 DevContainer 就绪(默认为 false 直接返回“未就绪”,否则阻塞等待)
// -- UserPublicKey
// 注意:必须携带 用户登录凭证
func GetDevcontainer(ctx *gitea_web_context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
if ctx == nil || ctx.Doer == nil {
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 2. 取得参数
repoIdStr := ctx.FormString("repoId")
UserPublicKey := ctx.FormString("userPublicKey")
log.Info(UserPublicKey)
repoId, err := strconv.ParseInt(repoIdStr, 10, 64)
if err != nil || repoId <= 0 {
Result.RespFailedIllegalParams.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
repoDevcontainerVO, err := devcontainer_service.OpenDevcontainerAPIService(ctx, ctx.Doer.ID, repoId)
if err != nil {
failureGetDevcontainer := Result.ResultType{
Code: Result.RespFailedOpenDevcontainer.Code,
Msg: Result.RespFailedOpenDevcontainer.Msg,
Data: map[string]any{
"ErrorMsg": err.Error(),
},
}
failureGetDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 4. 封装返回成功信息
successGetDevContainer := Result.ResultType{
Code: Result.RespSuccess.Code,
Msg: Result.RespSuccess.Msg,
Data: repoDevcontainerVO,
}
successGetDevContainer.RespondJson2HttpResponseWriter(ctx.Resp)
}
// DeleteRepoDevcontainer 删除某仓库的 DevContainer
//
// DELETE /api/devcontainer
// 请求体参数:
// -- repoId: 需要为哪个仓库创建 DevContainer
// 注意:必须携带 用户登录凭证
func DeleteRepoDevcontainer(ctx *gitea_web_context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
if ctx == nil || ctx.Doer == nil {
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 2. 取得参数 repoId
repoIdStr := ctx.FormString("repoId")
repoId, err := strconv.ParseInt(repoIdStr, 10, 64)
if err != nil || repoId <= 0 {
Result.RespFailedIllegalParams.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
err = devcontainer_service.DeleteDevContainer(ctx, ctx.Doer.ID, repoId)
if err != nil {
failureDeleteDevcontainer := Result.ResultType{
Code: Result.RespFailedDeleteDevcontainer.Code,
Msg: Result.RespFailedDeleteDevcontainer.Msg,
Data: map[string]any{
"ErrorMsg": err.Error(),
},
}
failureDeleteDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 4. 删除成功,返回提示信息
Result.RespSuccess.RespondJson2HttpResponseWriter(ctx.Resp)
}
// ListUserDevcontainers 枚举已登录用户所有的 DevContainers
//
// GET /api/devcontainer/user
// 请求输入参数:
// - page: 当前第几页默认第1页从1开始计数
// - pageSize: 每页记录数(默认值 setting.UI.Admin.DevContainersPagingNum
func ListUserDevcontainers(ctx *gitea_web_context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
if ctx.Doer == nil {
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 2. 查询数据库 当前登录用户拥有写入权限的仓库
userPage := ctx.FormInt("page")
if userPage <= 0 {
userPage = 1
}
userPageSize := ctx.FormInt("page_size")
if userPageSize <= 0 || userPageSize > 50 {
userPageSize = 50
}
userDevcontainersVO, err := devcontainer_service.GetDevcontainersList(ctx, ctx.Doer, userPage, userPageSize)
if err != nil {
resultFailed2ListUserDevcontainerList := Result.ResultType{
Code: Result.RespFailedListUserDevcontainers.Code,
Msg: Result.RespFailedListUserDevcontainers.Msg,
Data: map[string]string{
"ErrorMsg": err.Error(),
},
}
resultFailed2ListUserDevcontainerList.RespondJson2HttpResponseWriter(ctx.Resp)
return
}
// 3. 封装VO
resultListUserDevcontainersVO := Result.ResultType{
Code: Result.RespSuccess.Code,
Msg: Result.RespSuccess.Msg,
Data: userDevcontainersVO,
}
// 4. JSON序列化写入输出流
responseWriter := ctx.Resp
resultListUserDevcontainersVO.RespondJson2HttpResponseWriter(responseWriter)
}

repo.diff.view_file

@@ -245,6 +245,7 @@ func EditUser(ctx *context.APIContext) {
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
AllowCreateDevcontainer: optional.FromPtr(form.AllowCreateDevcontainer),
IsRestricted: optional.FromPtr(form.Restricted),
}

repo.diff.view_file

@@ -0,0 +1,33 @@
package entity
// 错误码 110xx 表示 devContainer 相关错误信息
// RespFailedIllegalParams 仓库ID参数无效
var RespFailedIllegalParams = ResultType{
Code: 11002,
Msg: "无效参数",
}
// RespFailedCreateDevcontainer 创建 DevContainer 失败
var RespFailedCreateDevcontainer = ResultType{
Code: 11003,
Msg: "创建 DevContainer 失败",
}
// RespFailedOpenDevcontainer 打开 DevContainer 失败
var RespFailedOpenDevcontainer = ResultType{
Code: 11004,
Msg: "打开 DevContainer 失败",
}
// RespFailedDeleteDevcontainer 删除 DevContainer 失败
var RespFailedDeleteDevcontainer = ResultType{
Code: 11005,
Msg: "删除 DevContainer 失败",
}
// RespFailedListUserDevcontainers 查询用户 DevContainer 列表失败
var RespFailedListUserDevcontainers = ResultType{
Code: 11006,
Msg: "查询用户 DevContainer 列表失败",
}

58
routers/entity/result.go Normal file
repo.diff.view_file

@@ -0,0 +1,58 @@
package entity
import (
"encoding/json"
"net/http"
"strings"
"code.gitea.io/gitea/modules/setting"
)
// ResultType 定义了响应格式
type ResultType struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"` // Data 字段可选
}
// RespondJson2HttpResponseWriter
// 将 ResultType 响应转换为 JSON 并写入响应
func (t *ResultType) RespondJson2HttpResponseWriter(w http.ResponseWriter) {
responseBytes, err := json.Marshal(*t)
if err != nil {
// 只有序列化 ResultType 失败时候, 才返回 HTTP 500 Internal Server Error
http.Error(w, http.StatusText(http.StatusInternalServerError)+": failed to marshal JSON", http.StatusInternalServerError)
return
}
// 序列化 ResultType 成功,无论成功或者失败,统一返回 HTTP 200 OK
if setting.CORSConfig.Enabled {
AllowOrigin := setting.CORSConfig.AllowDomain[0]
if AllowOrigin == "" {
AllowOrigin = "*"
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Access-Control-Allow-Origin", AllowOrigin)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(setting.CORSConfig.Methods, ","))
w.Header().Set("Access-Control-Allow-Headers", strings.Join(setting.CORSConfig.Headers, ","))
} else {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(responseBytes)
}
// RespSuccess 操作成功常量
var RespSuccess = ResultType{
Code: 0,
Msg: "操作成功",
}
// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权
var RespUnauthorizedFailure = ResultType{
Code: 00001,
Msg: "未登录,禁止访问",
}

repo.diff.view_file

@@ -5,6 +5,7 @@
package install
import (
other_context "context"
"net/http"
"net/mail"
"os"
@@ -13,13 +14,13 @@ import (
"slices"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models/db"
db_install "code.gitea.io/gitea/models/db/install"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password/hash"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
@@ -35,6 +36,7 @@ import (
"code.gitea.io/gitea/routers/common"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
"code.gitea.io/gitea/services/forms"
runners_service "code.gitea.io/gitea/services/runners"
"code.gitea.io/gitea/services/versioned_migration"
@@ -225,6 +227,17 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool {
return true
}
func checkDocker(ctx *context.Context, form *forms.InstallForm) bool {
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
ctx.Data["Err_DevContainer"] = true
return false
}
cli.Close()
return true
}
// SubmitInstall response for submit install items
func SubmitInstall(ctx *context.Context) {
if setting.InstallLock {
@@ -613,7 +626,14 @@ func SubmitInstall(ctx *context.Context) {
}
}
runners_service.RegistGlobalRunner(ctx)
if form.K8sEnable {
//K8s环境检测
} else {
if !checkDocker(ctx, &form) {
ctx.RenderWithErr("There is no docker environment", tplInstall, &form)
return
}
}
setting.ClearEnvConfigKeys()
log.Info("First-time run install finished!")
@@ -622,7 +642,17 @@ func SubmitInstall(ctx *context.Context) {
go func() {
// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
// What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future ....
time.Sleep(3 * time.Second)
otherCtx := other_context.Background()
if form.K8sEnable {
//K8s环境检测
} else {
err = devcontainer_service.RegistWebTerminal(otherCtx)
if err != nil {
ctx.RenderWithErr(ctx.Tr("install.web_terminal_failed", err), tplInstall, &form)
return
}
}
runners_service.RegistGlobalRunner(otherCtx)
// Now get the http.Server from this request and shut it down
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown

repo.diff.view_file

@@ -224,7 +224,6 @@ func prepareUserInfo(ctx *context.Context) *user_model.User {
return nil
}
ctx.Data["User"] = u
if u.LoginSource > 0 {
ctx.Data["LoginSource"], err = auth.GetSourceByID(ctx, u.LoginSource)
if err != nil {
@@ -437,6 +436,7 @@ func EditUserPost(ctx *context.Context) {
AllowImportLocal: optional.Some(form.AllowImportLocal),
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
AllowCreateDevcontainer: optional.Some(form.AllowCreateDevcontainer),
IsRestricted: optional.Some(form.Restricted),
Visibility: optional.Some(form.Visibility),
Language: optional.Some(form.Language),

repo.diff.view_file

@@ -0,0 +1,329 @@
package devcontainer
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"strconv"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
)
const (
tplGetDevContainerDetails templates.TplName = "repo/devcontainer/details"
)
// 获取仓库 Dev Container 详细信息
// GET /{username}/{reponame}/devcontainer
func GetDevContainerDetails(ctx *context.Context) {
if ctx.Doer == nil {
ctx.HTML(http.StatusForbidden, "")
return
}
var err error
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
log.Info("setting.CustomConf %s", setting.CustomConf)
log.Info("cfg.Section().Key().Value() %s", cfg.Section("server").Key("ROOT_URL").Value())
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["isAdmin"], err = devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["HasDevContainer"], err = devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["ValidateDevContainerConfiguration"] = true
ctx.Data["HasDevContainerConfiguration"], err = devcontainer_service.HasDevContainerConfiguration(ctx, ctx.Repo)
if err != nil {
log.Info(err.Error())
ctx.Data["ValidateDevContainerConfiguration"] = false
ctx.Flash.Error(err.Error(), true)
}
if ctx.Data["HasDevContainerConfiguration"] == false {
ctx.Data["ValidateDevContainerConfiguration"] = false
}
ctx.Data["HasDevContainerDockerfile"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
if ctx.Data["HasDevContainer"] == true {
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
imageName := configurationModel.Image
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
log.Info("%v %v", repo, tag)
ctx.Data["RepositoryAddress"] = registry
ctx.Data["RepositoryUsername"] = namespace
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
} else {
ctx.Data["WebSSHUrl"] = webTerminalURL
}
} else {
webTerminalContainerName := cfg.Section("devcontainer").Key("WEB_TERMINAL_CONTAINER").Value()
isWebTerminalNotFound, err := devcontainer_service.IsContainerNotFound(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
var webTerminalStatus string
if !isWebTerminalNotFound {
webTerminalStatus, err = devcontainer_service.GetDevContainerStatusFromDocker(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
}
if webTerminalContainerName == "" || isWebTerminalNotFound {
ctx.Flash.Error("webTerminal do not exist. creating ....", true)
err = devcontainer_service.RegistWebTerminal(ctx)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
} else if webTerminalStatus != "running" && webTerminalStatus != "restarting" {
err = devcontainer_service.DeleteDevContainerByDocker(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
err = devcontainer_service.RegistWebTerminal(ctx)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
} else {
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
terminalParams := "user=" +
ctx.Doer.Name +
"&repo=" +
ctx.Repo.Repository.Name +
"&repoid=" +
strconv.FormatInt(ctx.Repo.Repository.ID, 10) +
"&userid=" +
strconv.FormatInt(ctx.Doer.ID, 10) +
"&domain=" +
cfg.Section("server").Key("DOMAIN").Value() +
"&port=" +
rootPort
port, err := devcontainer_service.GetMappedPort(ctx, webTerminalContainerName, "7681")
webTerminalURL, err := devcontainer_service.ReplacePortOfUrl(cfg.Section("server").Key("ROOT_URL").Value(), fmt.Sprintf("%d", port))
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
}
}
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
if err == nil {
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
ctx.Data["CursorUrl"] = "cursor" + terminalURL
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
}
}
// 3. 携带数据渲染页面,返回
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
ctx.Data["PageIsDevContainer"] = true
ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["CreateDevcontainerSettingUrl"] = "/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name + "/devcontainer/createConfiguration"
ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/.devcontainer/devcontainer.json"
ctx.Data["TreeNames"] = []string{".devcontainer", "devcontainer.json"}
ctx.Data["TreePaths"] = []string{".devcontainer", ".devcontainer/devcontainer.json"}
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src"
ctx.Data["SaveMethods"] = []string{"Container", "DockerFile"}
ctx.Data["SaveMethod"] = "Container"
ctx.HTML(http.StatusOK, tplGetDevContainerDetails)
}
func GetDevContainerStatus(ctx *context.Context) {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
var userID string
if ctx.Doer != nil {
userID = fmt.Sprintf("%d", ctx.Doer.ID)
} else {
query := ctx.Req.URL.Query()
userID = query.Get("user")
}
realTimeStatus, err := devcontainer_service.GetDevContainerStatus(ctx, userID, fmt.Sprintf("%d", ctx.Repo.Repository.ID))
if err != nil {
log.Info("%v\n", err)
}
ctx.JSON(http.StatusOK, map[string]string{"status": realTimeStatus})
}
func CreateDevContainerConfiguration(ctx *context.Context) {
hasDevContainerConfiguration, err := devcontainer_service.HasDevContainerConfiguration(ctx, ctx.Repo)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if hasDevContainerConfiguration {
ctx.Flash.Error("Already exist", true)
return
}
isAdmin, err := devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !isAdmin {
ctx.Flash.Error("permisson denied", true)
return
}
err = devcontainer_service.CreateDevcontainerConfiguration(ctx.Repo.Repository, ctx.Doer)
if err != nil {
ctx.Flash.Error(err.Error(), true)
return
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func CreateDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if hasDevContainer {
ctx.Flash.Error("Already exist", true)
return
}
err = devcontainer_service.CreateDevcontainerAPIService(ctx, ctx.Repo.Repository, ctx.Doer, []string{}, true)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func DeleteDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
ctx.Flash.Error("Already Deleted.", true)
return
}
err = devcontainer_service.DeleteDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func RestartDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
log.Info(err.Error())
ctx.Flash.Error("Already delete", true)
return
}
err = devcontainer_service.RestartDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.JSON(http.StatusOK, "")
}
func StopDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
ctx.Flash.Error("Already delete", true)
return
}
err = devcontainer_service.StopDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.JSON(http.StatusOK, "")
}
func UpdateDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
if !hasDevContainer {
ctx.JSON(http.StatusOK, map[string]string{"message": "Already delete"})
return
}
// 取得参数
body, _ := io.ReadAll(ctx.Req.Body)
var updateInfo devcontainer_service.UpdateInfo
err = json.Unmarshal(body, &updateInfo)
if err != nil {
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo.Repository, &updateInfo)
if err != nil {
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
ctx.JSON(http.StatusOK, map[string]string{"redirect": ctx.Repo.RepoLink + "/devcontainer", "message": "成功"})
}
func GetTerminalCommand(ctx *context.Context) {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
query := ctx.Req.URL.Query()
cmd, status, err := devcontainer_service.GetTerminalCommand(ctx, query.Get("user"), ctx.Repo.Repository)
if err != nil {
log.Info(err.Error())
status = "error"
}
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status})
}
func GetDevContainerOutput(ctx *context.Context) {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
log.Info(err.Error())
}
ctx.JSON(http.StatusOK, output)
}

repo.diff.view_file

@@ -0,0 +1,21 @@
package devcontainer
import (
"net/http"
"code.gitea.io/gitea/modules/templates"
gitea_web_context "code.gitea.io/gitea/services/context"
)
const (
// TplDevstarHome 显示 DevStar Home 页面 templates/vscode-home.tmpl
TplDevstarHome templates.TplName = "repo/devcontainer/vscode-home"
)
// DevstarHome 渲染适配于 VSCode 插件的 DevStar Home 页面
func DevstarHome(ctx *gitea_web_context.Context) {
ctx.Data["Title"] = ctx.Tr("home")
ctx.Resp.Header().Del("X-Frame-Options")
//ctx.Resp.Header().Set("Content-Security-Policy", "frame-ancestors *")
ctx.HTML(http.StatusOK, TplDevstarHome)
}

repo.diff.view_file

@@ -23,9 +23,11 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
devcontainer_api "code.gitea.io/gitea/routers/api/devcontainer"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/admin"
"code.gitea.io/gitea/routers/web/auth"
devcontainer_web "code.gitea.io/gitea/routers/web/devcontainer"
"code.gitea.io/gitea/routers/web/devtest"
"code.gitea.io/gitea/routers/web/events"
"code.gitea.io/gitea/routers/web/explore"
@@ -865,7 +867,6 @@ func registerWebRoutes(m *web.Router) {
reqUnitPullsReader := context.RequireUnitReader(unit.TypePullRequests)
reqUnitWikiReader := context.RequireUnitReader(unit.TypeWiki)
reqUnitWikiWriter := context.RequireUnitWriter(unit.TypeWiki)
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
return func(ctx *context.Context) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
@@ -1390,6 +1391,45 @@ func registerWebRoutes(m *web.Router) {
}, reqSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code
m.Group("/{username}/{reponame}/devcontainer", func() { // repo Dev Container
m.Group("", func() {
m.Get("", devcontainer_web.GetDevContainerDetails)
m.Get("/createConfiguration", devcontainer_web.CreateDevContainerConfiguration)
m.Get("/create", devcontainer_web.CreateDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
m.Post("/delete", devcontainer_web.DeleteDevContainer)
m.Get("/restart", devcontainer_web.RestartDevContainer)
m.Get("/stop", devcontainer_web.StopDevContainer)
m.Post("/update", devcontainer_web.UpdateDevContainer)
},
// 已登录
context.RequireDevcontainerAccess, reqSignIn)
m.Get("/status", devcontainer_web.GetDevContainerStatus)
m.Get("/command", devcontainer_web.GetTerminalCommand)
m.Get("/output", devcontainer_web.GetDevContainerOutput)
},
// 解析仓库信息
// 具有code读取权限
context.RepoAssignment, reqUnitCodeReader,
)
m.Get("/devstar-home", devcontainer_web.DevstarHome)
m.Group("/api/devcontainer", func() {
// 获取 某用户在某仓库中的 DevContainer 细节包括SSH连接信息默认不会等待 (wait = false)
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId直接从 token 中提取
m.Get("", devcontainer_api.GetDevcontainer)
// 抽象方法,创建 某用户在某仓库中的 DevContainer
// 请求方式: POST /api/devcontainer
// 请求体数据: { repoId: REPO_ID }
m.Post("", web.Bind(forms.CreateRepoDevcontainerForm{}), devcontainer_api.CreateRepoDevcontainer)
// 删除某用户在某仓库中的 DevContainer
// 请求方法: DELETE /api/devcontainer?repoId=${repoId}
m.Delete("", devcontainer_api.DeleteRepoDevcontainer)
// 列举某用户已创建的所有 DevContainer
m.Get("/user", devcontainer_api.ListUserDevcontainers)
})
m.Group("/{username}/{reponame}", func() { // repo tags
m.Group("/tags", func() {
m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList)

repo.diff.view_file

@@ -12,6 +12,7 @@ import (
"net/http"
"net/url"
"path"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
@@ -396,6 +397,26 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
return
}
ctx.Data["Permission"] = &ctx.Repo.Permission
if ctx.Doer != nil {
ctx.Data["AllowCreateDevcontainer"] = ctx.Doer.AllowCreateDevcontainer
} else {
query := ctx.Req.URL.Query()
userID := query.Get("user")
userNum, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
return
}
u, err := user_model.GetUserByID(ctx, userNum)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
} else {
ctx.ServerError("GetUserByID", err)
}
return
}
ctx.Data["AllowCreateDevcontainer"] = u.AllowCreateDevcontainer
}
if repo.IsMirror {
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
@@ -412,6 +433,18 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
}
func RequireDevcontainerAccess(ctx *Context) {
if ctx.Doer == nil {
ctx.HTTPError(http.StatusUnauthorized, "Devcontainer access requires login")
return
}
if !ctx.Doer.CanCreateDevcontainer() {
ctx.HTTPError(http.StatusForbidden, "User cannot create devcontainers")
return
}
}
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) {
if ctx.Data["Repository"] != nil {

repo.diff.view_file

@@ -0,0 +1,893 @@
package devcontainer
import (
"archive/tar"
"bytes"
"context"
"fmt"
"math"
"net"
"net/url"
"os"
"strings"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/docker/docker/api/types"
"xorm.io/builder"
)
func HasDevContainer(ctx context.Context, userID, repoID int64) (bool, error) {
var hasDevContainer bool
dbEngine := db.GetEngine(ctx)
hasDevContainer, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Exist()
if err != nil {
return hasDevContainer, err
}
return hasDevContainer, nil
}
func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return true, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return true, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return true, fmt.Errorf("配置格式错误")
} else {
return true, nil
}
}
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return false, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return false, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return false, fmt.Errorf("配置格式错误")
} else {
log.Info("%v", configurationModel)
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
return false, nil
}
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
}
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
jsonString := `{
"name":"template",
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
"forwardPorts": ["8080"],
"containerEnv": {
"NODE_ENV": "development"
},
"initializeCommand": "echo \"init\";",
"postCreateCommand": [
"echo \"created\"",
"echo \"test\""
],
"runArgs": [
"-p 8888"
]
}`
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(jsonString)),
},
},
OldBranch: "main",
NewBranch: "main",
Message: "add container configuration",
})
if err != nil {
return err
}
return nil
}
func GetWebTerminalURL(ctx context.Context, userID, repoID int64) (string, error) {
var devcontainerName string
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", err
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devcontainerName)
if err != nil {
return "", err
}
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
}
return "", nil
}
/*
-1不存在
0 已创建数据库记录
1 正在拉取镜像
2 正在创建和启动容器
3 容器安装必要工具
4 容器正在运行
5 正在提交容器更新
6 正在重启
7 正在停止
8 容器已停止
9 正在删除
10已删除
*/
func GetDevContainerStatus(ctx context.Context, userID, repoID string) (string, error) {
var id int
var containerName string
var status uint16
var realTimeStatus uint16
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", err
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Select("devcontainer_status, id, name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&status, &id, &containerName)
if err != nil {
return "", err
}
if id == 0 {
return fmt.Sprintf("%d", -1), nil
}
realTimeStatus = status
switch status {
//正在重启
case 6:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "running" {
realTimeStatus = 4
}
}
break
//正在关闭
case 7:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "exited" {
realTimeStatus = 8
} else {
err = StopDevContainerByDocker(ctx, containerName)
if err != nil {
log.Info(err.Error())
}
}
}
break
case 9:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
isContainerNotFound, err := IsContainerNotFound(ctx, containerName)
if err != nil {
return "", err
} else if isContainerNotFound {
realTimeStatus = 10
} else {
err = DeleteDevContainerByDocker(ctx, containerName)
if err != nil {
log.Info(err.Error())
}
}
}
break
default:
log.Info("other status")
}
//状态更新
if realTimeStatus != status {
if realTimeStatus == 10 {
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
return "-1", nil
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, status).
Update(&devcontainer_models.DevcontainerOutput{Status: "finished"})
if err != nil {
return "", err
}
}
return fmt.Sprintf("%d", realTimeStatus), nil
}
func CreateDevContainer(ctx context.Context, repo *repo.Repository, doer *user.User, publicKeyList []string, isWebTerminal bool) error {
containerName := getSanitizedDevcontainerName(doer.Name, repo.Name)
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
unixTimestamp := time.Now().Unix()
newDevcontainer := devcontainer_models.Devcontainer{
Name: containerName,
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root",
DevcontainerWorkDir: "/workspace",
DevcontainerStatus: 0,
RepoId: repo.ID,
UserId: doer.ID,
CreatedUnix: unixTimestamp,
UpdatedUnix: unixTimestamp,
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Insert(newDevcontainer)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&newDevcontainer)
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
imageName, err := CreateDevContainerByDockerCommand(otherCtx, &newDevcontainer, repo, publicKeyList)
if err != nil {
return
}
if !isWebTerminal {
CreateDevContainerByDockerAPI(otherCtx, &newDevcontainer, imageName, repo, publicKeyList)
}
}
}()
return nil
}
func DeleteDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 9})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
err = DeleteDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func RestartDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 6})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
err = RestartDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func StopDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 7})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
err = StopDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
if err != nil {
return err
}
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
if err != nil {
return err
}
if updateErr != nil {
return updateErr
}
}
return nil
}
func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repository) (string, string, error) {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", "", err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return "", "", err
}
realTimeStatus := devContainerInfo.DevcontainerStatus
var cmd string
switch devContainerInfo.DevcontainerStatus {
case 0:
if devContainerInfo.Id > 0 {
realTimeStatus = 1
}
break
case 1:
//正在拉取镜像,当镜像拉取成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return "", "", err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return "", "", err
}
var imageName string
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
imageName = configurationModel.Image
} else {
imageName = userID + "-" + fmt.Sprintf("%d", repo.ID) + "-dockerfile"
}
isExist, err := ImageExists(ctx, imageName)
if err != nil {
return "", "", err
}
if isExist {
realTimeStatus = 2
} else {
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
}
}
break
case 2:
//正在创建容器,创建容器成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
if err != nil {
return "", "", err
}
if status == "running" {
//添加脚本文件
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
} else {
var scriptContent []byte
_, err = os.Stat("webTerminal.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/webTerminal.sh")
if os.IsNotExist(err) {
return "", "", err
} else {
scriptContent, err = os.ReadFile("/app/gitea/webTerminal.sh")
if err != nil {
return "", "", err
}
}
} else {
scriptContent, err = os.ReadFile("webTerminal.sh")
if err != nil {
return "", "", err
}
}
// 创建 tar 归档文件
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加文件到 tar 归档
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", "", err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
if err != nil {
return "", "", err
}
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
if err != nil {
log.Info("%v", err)
return "", "", err
}
}
realTimeStatus = 3
}
}
break
case 3:
//正在初始化容器,初始化容器成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir)
if err != nil {
return "", "", err
}
if status {
realTimeStatus = 4
}
}
break
case 4:
//正在连接容器
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
}
break
}
if realTimeStatus != devContainerInfo.DevcontainerStatus {
//下一条指令
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", "", err
}
}
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
}
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(ctx)
resp := OutputResponse{}
var status string
var containerName string
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, name").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&status, &containerName)
if err != nil {
return resp, err
}
err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Find(&devContainerOutput)
if err != nil {
return resp, err
}
if len(devContainerOutput) > 0 {
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
resp.CurrentJob.Detail = status
if status == "4" {
// 获取WebSSH服务端口
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
if err == nil {
return resp, err
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
return resp, err
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
return resp, err
}
}
for _, item := range devContainerOutput {
logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{
Index: 1,
Message: item.Output,
})
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
Summary: item.Command,
Status: item.Status,
Logs: logLines,
})
}
}
return resp, nil
}
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return 0, err
}
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
return 0, nil
} else {
port, err := docker_module.GetMappedPort(ctx, containerName, port)
if err != nil {
return 0, err
}
return port, nil
}
}
func GetDevcontainersList(ctx context.Context, doer *user.User, pageNum, pageSize int) (DevcontainerList, error) {
// 0. 构造异常返回时的空数据
var resultDevContainerListVO = DevcontainerList{
Page: 0,
PageSize: 50,
PageTotalNum: 0,
ItemTotalNum: 0,
DevContainers: []devcontainer_models.Devcontainer{},
}
resultDevContainerListVO.UserID = doer.ID
resultDevContainerListVO.Username = doer.Name
paginationOption := db.ListOptions{
Page: pageNum,
PageSize: pageSize,
}
paginationOption.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers
if paginationOption.Page <= 0 { // 未指定页码/无效页码:查询第 1 页
paginationOption.Page = 1
}
if paginationOption.PageSize <= 0 || paginationOption.PageSize > 50 {
paginationOption.PageSize = 50 // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小
}
resultDevContainerListVO.Page = paginationOption.Page
resultDevContainerListVO.PageSize = paginationOption.PageSize
// 2. SQL 条件构建
sqlCondition := builder.Eq{"user_id": doer.ID}
// 执行数据库事务
err := db.WithTx(ctx, func(ctx context.Context) error {
// 查询总数
count, err := db.GetEngine(ctx).
Table("devcontainer").
Where(sqlCondition).
Count()
if err != nil {
return err
}
resultDevContainerListVO.ItemTotalNum = count
// 无记录直接返回
if count == 0 {
return nil
}
// 计算分页参数
pageSize := int64(resultDevContainerListVO.PageSize)
resultDevContainerListVO.PageTotalNum = int(math.Ceil(float64(count) / float64(pageSize)))
// 查询分页数据
sess := db.GetEngine(ctx).
Table("devcontainer").
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
Where(sqlCondition).
OrderBy("devcontainer_id DESC").
Select(`devcontainer.id AS devcontainer_id,
devcontainer.name AS devcontainer_name,
devcontainer.devcontainer_host AS devcontainer_host,
devcontainer.devcontainer_username AS devcontainer_username,
devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
devcontainer.repo_id AS repo_id,
repository.name AS repo_name,
repository.owner_name AS repo_owner_name,
repository.description AS repo_description,
CONCAT('/', repository.owner_name, '/', repository.name) AS repo_link`)
resultDevContainerListVO.DevContainers = make([]devcontainer_models.Devcontainer, 0, pageSize)
err = db.SetSessionPagination(sess, &paginationOption).
Find(&resultDevContainerListVO.DevContainers)
if err != nil {
return err
}
return nil
})
if err != nil {
return resultDevContainerListVO, err
}
return resultDevContainerListVO, nil
}
func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gitea_context.Repository) (string, error) {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID).
Get(&devContainerInfo)
if err != nil {
return "", err
}
// 加载配置文件
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err)
return "", err
}
log.Info("Get_IDE_TerminalURL: 配置文件加载成功, ROOT_URL=%s", cfg.Section("server").Key("ROOT_URL").Value())
var access_token string
// 检查 session 中是否已存在 token
if ctx.Session.Get("access_token") != nil {
access_token = ctx.Session.Get("access_token").(string)
} else {
// 生成 token
token := &auth_model.AccessToken{
UID: devContainerInfo.UserId,
Name: "terminal_login_token",
}
exist, err := auth_model.AccessTokenByNameExists(ctx, token)
if err != nil {
return "", err
}
if exist {
db.GetEngine(ctx).Table("access_token").Where("uid = ? AND name = ?", doer.ID, "terminal_login_token").Delete()
}
scope, err := auth_model.AccessTokenScope(strings.Join([]string{"write:user", "write:repository"}, ",")).Normalize()
if err != nil {
return "", err
}
token.Scope = scope
err = auth_model.NewAccessToken(db.DefaultContext, token)
if err != nil {
return "", err
}
ctx.Session.Set("terminal_login_token", token.Token)
access_token = token.Token
}
// 根据不同的代理类型获取 SSH 端口
var port string
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
} else {
mappedPort, err := docker_module.GetMappedPort(ctx, devContainerInfo.Name, "22")
if err != nil {
return "", err
}
port = fmt.Sprintf("%d", mappedPort)
}
// 构建并返回 URL
return "://mengning.devstar/" +
"openProject?host=" + repo.Repository.Name +
"&hostname=" + devContainerInfo.DevcontainerHost +
"&port=" + port +
"&username=" + doer.Name +
"&path=" + devContainerInfo.DevcontainerWorkDir +
"&access_token=" + access_token +
"&devstar_username=" + repo.Repository.OwnerName +
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
}

repo.diff.view_file

@@ -0,0 +1,57 @@
package devcontainer
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
)
func CreateDevcontainerAPIService(ctx context.Context, repo *repo.Repository, doer *user.User, publicKeyList []string, isWebTerminal bool) error {
var userSSHPublicKeyList []string
err := db.GetEngine(ctx).
Table("public_key").
Select("content").
Where("owner_id = ?", doer.ID).
Find(&userSSHPublicKeyList)
if err != nil {
return err
}
allPublicKeyList := append(userSSHPublicKeyList, publicKeyList...)
return CreateDevContainer(ctx, repo, doer, allPublicKeyList, isWebTerminal)
}
// OpenDevcontainerAPIService API 专用获取 DevContainer Service
func OpenDevcontainerAPIService(ctx context.Context, userID, repoID int64) (*DevcontainerVO, error) {
var devcontainerDetails DevcontainerVO
dbEngine := db.GetEngine(ctx)
// 2. 查询数据库
_, err := dbEngine.
Table("devcontainer").
Select(""+
"devcontainer.id AS devcontainer_id,"+
"devcontainer.name AS devcontainer_name,"+
"devcontainer.devcontainer_host AS devcontainer_host,"+
"devcontainer.devcontainer_status AS devcontainer_status,"+
"devcontainer.devcontainer_username AS devcontainer_username,"+
"devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
"devcontainer.repo_id AS repo_id,"+
"devcontainer.user_id AS user_id,"+
"repository.name AS repo_name,"+
"repository.owner_name AS repo_owner_name,"+
"repository.description AS repo_description,"+
"CONCAT('/', repository.owner_name, '/', repository.name) AS repo_link").
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
Where("devcontainer.user_id = ? AND devcontainer.repo_id = ?", userID, repoID).
Get(&devcontainerDetails)
if err != nil {
return &devcontainerDetails, err
}
// 2. 获取实时port
devcontainerDetails.DevContainerPort, err = GetMappedPort(ctx, devcontainerDetails.DevContainerName, "22")
if err != nil {
return &devcontainerDetails, err
}
return &devcontainerDetails, nil
}

repo.diff.view_file

@@ -0,0 +1,56 @@
package devcontainer
import (
"bytes"
"context"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
files_service "code.gitea.io/gitea/services/repository/files"
)
func GetDevcontainerConfigurationString(ctx context.Context, repo *repo.Repository) (string, error) {
configuration, err := GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json")
if err != nil {
return "", err
}
cleanedContent, err := removeComments(configuration)
if err != nil {
return "", err
}
return cleanedContent, nil
}
// 移除 JSON 文件中的注释
func removeComments(data string) (string, error) {
// 移除单行注释 // ...
re := regexp.MustCompile(`//.*`)
data = re.ReplaceAllString(data, "")
// 移除多行注释 /* ... */
re = regexp.MustCompile(`/\*.*?\*/`)
data = re.ReplaceAllString(data, "")
return data, nil
}
func UpdateDevcontainerConfiguration(newContent string, repo *repo.Repository, doer *user.User) error {
// 更新devcontainer.json配置文件
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "update",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(newContent)),
},
},
OldBranch: repo.DefaultBranch,
Message: "Update container",
})
if err != nil {
return err
}
return nil
}

repo.diff.view_file

@@ -0,0 +1,865 @@
package devcontainer
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
)
// DevContainerConfiguration 表示 devcontainer.json 的根配置
type DevContainerConfiguration struct {
// ----------------------------
// 通用属性 (General properties)
// ----------------------------
Name string `json:"name,omitempty"` // 容器名称
ForwardPorts []interface{} `json:"forwardPorts,omitempty"` // 要转发的端口(整数或字符串)
PortsAttributes map[string]PortAttribute `json:"portsAttributes,omitempty"` // 端口特性配置
OtherPortsAttributes map[string]PortAttribute `json:"otherPortsAttributes,omitempty"` // 其他端口特性配置
ContainerEnv map[string]string `json:"containerEnv,omitempty"` // 容器环境变量
RemoteEnv map[string]string `json:"remoteEnv,omitempty"` // 远程环境变量
RemoteUser string `json:"remoteUser,omitempty"` // 远程用户
ContainerUser string `json:"containerUser,omitempty"` // 容器用户
UpdateRemoteUserUID bool `json:"updateRemoteUserUID,omitempty"` // 是否更新远程用户UID
UserEnvProbe string `json:"userEnvProbe,omitempty"` // 用户环境探测方式(枚举值)
OverrideCommand bool `json:"overrideCommand,omitempty"` // 是否覆盖默认命令
ShutdownAction string `json:"shutdownAction,omitempty"` // 关闭动作(枚举值)
Init bool `json:"init,omitempty"` // 是否使用init进程
Privileged bool `json:"privileged,omitempty"` // 是否特权模式
CapAdd []string `json:"capAdd,omitempty"` // 添加的Linux能力
SecurityOpt []string `json:"securityOpt,omitempty"` // 安全选项
Mounts []interface{} `json:"mounts,omitempty"` // 挂载配置(字符串或对象)
Features map[string]json.RawMessage `json:"features,omitempty"` // 功能特性配置
OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"` // 特性安装顺序覆盖
Customizations map[string]interface{} `json:"customizations,omitempty"` // 自定义配置
// --------------------------------------
// 镜像/Dockerfile 相关属性 (Image/Dockerfile)
// --------------------------------------
Image string `json:"image,omitempty"` // 使用的镜像
Build *Build `json:"build,omitempty"` // Docker构建配置
AppPort interface{} `json:"appPort,omitempty"` // 应用端口(整数/字符串/数组)
WorkspaceMount string `json:"workspaceMount,omitempty"` // 工作区挂载点
WorkspaceFolder string `json:"workspaceFolder,omitempty"` // 工作区目录
RunArgs []string `json:"runArgs,omitempty"` // 容器运行参数
// ----------------------------------
// Docker Compose 相关属性 (Docker Compose)
// ----------------------------------
DockerComposeFile StringOrArray `json:"dockerComposeFile,omitempty"` // Compose文件路径字符串或数组
Service string `json:"service,omitempty"` // 使用的服务名
RunServices []string `json:"runServices,omitempty"` // 要运行的服务
// ---------------------------
// 生命周期命令 (Lifecycle commands)
// ---------------------------
InitializeCommand interface{} `json:"initializeCommand,omitempty"` // 初始化命令(字符串/数组/对象)
OnCreateCommand interface{} `json:"onCreateCommand,omitempty"` // 创建后命令(字符串/数组/对象)
UpdateContentCommand interface{} `json:"updateContentCommand,omitempty"` // 内容更新命令(字符串/数组/对象)
PostCreateCommand interface{} `json:"postCreateCommand,omitempty"` // 创建完成命令(字符串/数组/对象)
PostStartCommand interface{} `json:"postStartCommand,omitempty"` // 启动后命令(字符串/数组/对象)
PostAttachCommand interface{} `json:"postAttachCommand,omitempty"` // 连接后命令(字符串/数组/对象)
WaitFor string `json:"waitFor,omitempty"` // 等待事件(枚举值)
// ---------------------------
// 主机要求 (Host requirements)
// ---------------------------
HostRequirements *HostRequirements `json:"hostRequirements,omitempty"` // 主机资源要求
}
// PortAttribute 端口特性配置
type PortAttribute struct {
Label string `json:"label,omitempty"` // 端口标签
Protocol string `json:"protocol,omitempty"` // 协议类型(枚举值)
OnAutoForward string `json:"onAutoForward,omitempty"` // 自动转发行为(枚举值)
RequireLocalPort bool `json:"requireLocalPort,omitempty"` // 是否需要本地端口
ElevateIfNeeded bool `json:"elevateIfNeeded,omitempty"` // 需要时是否提权
}
// Build Docker构建配置
type Build struct {
Dockerfile string `json:"dockerfile,omitempty"` // Dockerfile路径
Context string `json:"context,omitempty"` // 构建上下文
Args map[string]string `json:"args,omitempty"` // 构建参数
Options []string `json:"options,omitempty"` // 构建选项
Target string `json:"target,omitempty"` // 构建目标阶段
CacheFrom StringOrArray `json:"cacheFrom,omitempty"` // 缓存来源(字符串或数组)
}
// HostRequirements 主机资源要求
type HostRequirements struct {
CPUs int `json:"cpus,omitempty"` // CPU核心数
Memory string `json:"memory,omitempty"` // 内存要求(如"8GB"
Storage string `json:"storage,omitempty"` // 存储要求(如"100GB"
GPU interface{} `json:"gpu,omitempty"` // GPU要求布尔/字符串/对象)
}
// StringOrArray 处理字符串或数组类型的自定义类型
type StringOrArray []string
// MarshalJSON 自定义JSON序列化单个值时输出字符串
func (s StringOrArray) MarshalJSON() ([]byte, error) {
if len(s) == 1 {
return json.Marshal(s[0])
}
return json.Marshal([]string(s))
}
// UnmarshalJSON 自定义JSON反序列化
func (s *StringOrArray) UnmarshalJSON(data []byte) error {
// 尝试解析为字符串
var single string
if err := json.Unmarshal(data, &single); err == nil {
*s = []string{single}
return nil
}
// 尝试解析为字符串数组
var multi []string
if err := json.Unmarshal(data, &multi); err == nil {
*s = multi
return nil
}
return json.Unmarshal(data, &multi)
}
// Unmarshal 反序列化JSON
func UnmarshalDevcontainerConfigContent(devcontainerConfigContent string) (*DevContainerConfiguration, error) {
// 1. 初始化
devcontainerObject := &DevContainerConfiguration{}
// 2. JSON 反序列化
err := json.Unmarshal([]byte(devcontainerConfigContent), devcontainerObject)
if err != nil {
return nil, err
}
// 3. 返回
return devcontainerObject, err
}
// 定义允许的枚举值集合
var (
validUserEnvProbe = []string{"none", "interactiveShell", "loginShell"}
validShutdownActions = []string{"none", "stopContainer"}
validPortProtocols = []string{"http", "https", "none"}
validOnAutoForwardOpts = []string{"notify", "openBrowser", "openPreview", "ignore", "silent"}
validWaitForEvents = []string{"init", "create", "attach"}
)
// Validate 验证 DevContainerConfiguration 的合法性
func (c *DevContainerConfiguration) Validate() []error {
var errors []error
// 1. 基本字段验证
errors = append(errors, c.validateBasicFields()...)
// 2. 端口相关验证
errors = append(errors, c.validatePorts()...)
// 3. 构建配置验证
errors = append(errors, c.validateBuildConfig()...)
// 4. Docker Compose 配置验证
errors = append(errors, c.validateDockerCompose()...)
// 5. 生命周期命令验证
errors = append(errors, c.validateLifecycleCommands()...)
// 6. 主机要求验证
errors = append(errors, c.validateHostRequirements()...)
// 7. 互斥字段验证
errors = append(errors, c.validateMutuallyExclusive()...)
// 8. RunArgs字段验证
errors = append(errors, c.ValidateRunArgs()...)
return errors
}
// 验证基本字段
func (c *DevContainerConfiguration) validateBasicFields() []error {
var errors []error
// 名称验证
if c.Name == "" {
errors = append(errors, fmt.Errorf("name is required"))
} else if len(c.Name) > 100 {
errors = append(errors, fmt.Errorf("name exceeds maximum length of 100 characters"))
}
// 枚举值验证
errors = append(errors, validateEnum("userEnvProbe", c.UserEnvProbe, validUserEnvProbe)...)
errors = append(errors, validateEnum("shutdownAction", c.ShutdownAction, validShutdownActions)...)
// 用户ID更新验证
if c.UpdateRemoteUserUID && (c.RemoteUser == "" && c.ContainerUser == "") {
errors = append(errors,
fmt.Errorf("updateRemoteUserUID requires either remoteUser or containerUser to be set"))
}
// 挂载点验证
for i, mount := range c.Mounts {
switch m := mount.(type) {
case string:
if !isValidMountString(m) {
errors = append(errors,
fmt.Errorf("mount[%d]: invalid mount format '%s'", i, m))
}
case map[string]interface{}:
// 更详细的对象挂载验证
if _, ok := m["source"]; !ok {
errors = append(errors,
fmt.Errorf("mount[%d]: missing 'source' property", i))
}
if _, ok := m["target"]; !ok {
errors = append(errors,
fmt.Errorf("mount[%d]: missing 'target' property", i))
}
default:
errors = append(errors,
fmt.Errorf("mount[%d]: must be string or object", i))
}
}
return errors
}
// 验证端口相关配置
func (c *DevContainerConfiguration) validatePorts() []error {
var errors []error
// 转发端口格式验证
for i, port := range c.ForwardPorts {
switch p := port.(type) {
case int:
if p <= 0 || p > 65535 {
errors = append(errors,
fmt.Errorf("forwardPorts[%d]: port number %d out of range", i, p))
}
case string:
if !isValidPortSpec(p) {
errors = append(errors,
fmt.Errorf("forwardPorts[%d]: invalid port specification '%s'", i, p))
}
default:
errors = append(errors,
fmt.Errorf("forwardPorts[%d]: must be integer or string", i))
}
}
// 应用端口格式验证
if c.AppPort != nil {
switch appPort := c.AppPort.(type) {
case int:
if appPort <= 0 || appPort > 65535 {
errors = append(errors,
fmt.Errorf("appPort: port number %d out of range", appPort))
}
case string:
if !isValidPortSpec(appPort) {
errors = append(errors,
fmt.Errorf("appPort: invalid port specification '%s'", appPort))
}
case []interface{}:
for i, port := range appPort {
switch p := port.(type) {
case int:
if p <= 0 || p > 65535 {
errors = append(errors,
fmt.Errorf("appPort[%d]: port number %d out of range", i, p))
}
case string:
if !isValidPortSpec(p) {
errors = append(errors,
fmt.Errorf("appPort[%d]: invalid port specification '%s'", i, p))
}
default:
errors = append(errors,
fmt.Errorf("appPort[%d]: must be integer or string", i))
}
}
default:
errors = append(errors,
fmt.Errorf("appPort: must be integer, string, or array of port specifications"))
}
}
// 端口属性验证
validatePortAttributes := func(attrs map[string]PortAttribute, prefix string) {
for port, attr := range attrs {
// 验证端口格式
if !isValidPortSpec(port) {
errors = append(errors,
fmt.Errorf("%s: key '%s' is not a valid port specification", prefix, port))
}
// 验证属性
errors = append(errors,
validateEnum(prefix+"."+port+".protocol", attr.Protocol, validPortProtocols)...)
errors = append(errors,
validateEnum(prefix+"."+port+".onAutoForward", attr.OnAutoForward, validOnAutoForwardOpts)...)
}
}
validatePortAttributes(c.PortsAttributes, "portsAttributes")
validatePortAttributes(c.OtherPortsAttributes, "otherPortsAttributes")
return errors
}
// 验证构建配置
func (c *DevContainerConfiguration) validateBuildConfig() []error {
var errors []error
if c.Build != nil {
// Dockerfile 路径验证
if c.Build.Dockerfile == "" {
errors = append(errors, fmt.Errorf("build.dockerfile is required when using build configuration"))
}
// 构建目标验证
if c.Build.Target != "" && !isValidIdentifier(c.Build.Target) {
errors = append(errors,
fmt.Errorf("build.target: invalid target name '%s'", c.Build.Target))
}
// 构建参数验证
for argName := range c.Build.Args {
if !isValidEnvVarName(argName) {
errors = append(errors,
fmt.Errorf("build.args: invalid argument name '%s'", argName))
}
}
}
return errors
}
// 验证 Docker Compose 配置
func (c *DevContainerConfiguration) validateDockerCompose() []error {
var errors []error
if len(c.DockerComposeFile) > 0 {
// 服务名验证
if c.Service == "" {
errors = append(errors,
fmt.Errorf("service is required when using dockerComposeFile"))
} else if !isValidServiceName(c.Service) {
errors = append(errors,
fmt.Errorf("service: invalid service name '%s'", c.Service))
}
// 运行服务验证
for i, svc := range c.RunServices {
if !isValidServiceName(svc) {
errors = append(errors,
fmt.Errorf("runServices[%d]: invalid service name '%s'", i, svc))
}
}
}
return errors
}
// 验证生命周期命令
func (c *DevContainerConfiguration) validateLifecycleCommands() []error {
var errors []error
validateCommand := func(name string, cmd interface{}) {
switch cmd.(type) {
case nil:
// 允许为空
case string:
// 空字符串视为有效
case []interface{}:
// 验证数组元素都是字符串
arr := cmd.([]interface{})
for i, item := range arr {
if _, ok := item.(string); !ok {
errors = append(errors,
fmt.Errorf("%s[%d]: must be string", name, i))
}
}
case map[string]interface{}:
// 验证命令对象结构
cmdObj := cmd.(map[string]interface{})
if _, ok := cmdObj["command"]; !ok {
errors = append(errors,
fmt.Errorf("%s: command object requires 'command' property", name))
}
default:
errors = append(errors,
fmt.Errorf("%s: must be string, array of strings, or command object", name))
}
}
validateCommand("initializeCommand", c.InitializeCommand)
validateCommand("onCreateCommand", c.OnCreateCommand)
validateCommand("updateContentCommand", c.UpdateContentCommand)
validateCommand("postCreateCommand", c.PostCreateCommand)
validateCommand("postStartCommand", c.PostStartCommand)
validateCommand("postAttachCommand", c.PostAttachCommand)
// 等待事件验证
errors = append(errors, validateEnum("waitFor", c.WaitFor, validWaitForEvents)...)
return errors
}
// 验证主机要求
func (c *DevContainerConfiguration) validateHostRequirements() []error {
var errors []error
if c.HostRequirements != nil {
// CPU 验证
if c.HostRequirements.CPUs < 0 {
errors = append(errors,
fmt.Errorf("hostRequirements.cpus: must be positive integer"))
}
// 内存格式验证
if c.HostRequirements.Memory != "" {
if !isValidMemorySpec(c.HostRequirements.Memory) {
errors = append(errors,
fmt.Errorf("hostRequirements.memory: invalid format '%s'", c.HostRequirements.Memory))
}
}
// 存储格式验证
if c.HostRequirements.Storage != "" {
if !isValidStorageSpec(c.HostRequirements.Storage) {
errors = append(errors,
fmt.Errorf("hostRequirements.storage: invalid format '%s'", c.HostRequirements.Storage))
}
}
// GPU 验证
switch gpu := c.HostRequirements.GPU.(type) {
case bool:
// 布尔值有效
case string:
if !isValidGPUDriver(gpu) {
errors = append(errors,
fmt.Errorf("hostRequirements.gpu: unsupported driver '%s'", gpu))
}
case map[string]interface{}:
// 验证 GPU 对象结构
if _, ok := gpu["count"]; !ok {
errors = append(errors,
fmt.Errorf("hostRequirements.gpu: object requires 'count' property"))
}
case nil:
// 允许为空
default:
errors = append(errors,
fmt.Errorf("hostRequirements.gpu: must be boolean, string, or object"))
}
}
return errors
}
// 验证互斥字段
func (c *DevContainerConfiguration) validateMutuallyExclusive() []error {
var errors []error
// 镜像和构建配置互斥
if c.Image != "" && c.Build != nil {
errors = append(errors,
fmt.Errorf("cannot specify both 'image' and 'build' properties"))
}
// Docker Compose 和镜像/构建互斥
if len(c.DockerComposeFile) > 0 && (c.Image != "" || c.Build != nil) {
errors = append(errors,
fmt.Errorf("cannot specify 'dockerComposeFile' with 'image' or 'build'"))
}
// 工作区挂载和工作区文件夹共存验证
if c.WorkspaceMount != "" && c.WorkspaceFolder != "" {
errors = append(errors,
fmt.Errorf("'workspaceMount' and 'workspaceFolder' should not be used together"))
}
return errors
}
// ================ 辅助验证函数 ================
// 验证枚举值
func validateEnum(fieldName, value string, validValues []string) []error {
if value == "" {
return nil
}
for _, valid := range validValues {
if value == valid {
return nil
}
}
return []error{fmt.Errorf("%s: invalid value '%s', valid options: %v",
fieldName, value, validValues)}
}
// 验证端口规范格式 (支持 "8080" 或 "8080:80")
func isValidPortSpec(spec string) bool {
parts := strings.Split(spec, ":")
if len(parts) > 2 {
return false
}
for _, part := range parts {
port, err := strconv.Atoi(part)
if err != nil || port <= 0 || port > 65535 {
return false
}
}
return true
}
// 验证挂载字符串格式 (支持 "source:target" 或 "source:target:ro")
func isValidMountString(mount string) bool {
parts := strings.Split(mount, ":")
if len(parts) < 2 || len(parts) > 3 {
return false
}
// 验证选项(如果存在)
if len(parts) == 3 {
options := strings.Split(parts[2], ",")
for _, opt := range options {
switch opt {
case "ro", "rw", "consistent", "cached", "delegated":
// 有效选项
default:
return false
}
}
}
return true
}
// 验证环境变量名格式
func isValidEnvVarName(name string) bool {
match, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, name)
return match
}
// 验证服务名格式
func isValidServiceName(name string) bool {
match, _ := regexp.MatchString(`^[a-z0-9][a-z0-9_-]*$`, name)
return match
}
// 验证内存规格格式 (如 "4G", "512M")
func isValidMemorySpec(mem string) bool {
re := regexp.MustCompile(`^\d+[KMGTP]?B?$`)
return re.MatchString(strings.ToUpper(mem))
}
// 验证存储规格格式 (如 "100GB", "512MB")
func isValidStorageSpec(storage string) bool {
re := regexp.MustCompile(`^\d+[KMGTP]B$`)
return re.MatchString(strings.ToUpper(storage))
}
// 验证GPU驱动名
func isValidGPUDriver(driver string) bool {
validDrivers := []string{"nvidia", "amd", "intel"}
for _, d := range validDrivers {
if strings.EqualFold(driver, d) {
return true
}
}
return false
}
// isValidIdentifier 验证 Docker 构建目标名称的合法性
// Docker 目标名称必须遵循以下规则:
// 1. 只能包含小写字母、数字、下划线、连字符和点号
// 2. 必须以字母或数字开头
// 3. 不能超过 128 个字符
func isValidIdentifier(name string) bool {
if name == "" {
return false
}
// 长度限制 (Docker 规范)
if len(name) > 128 {
return false
}
// 检查首字符
firstChar := name[0]
if !((firstChar >= 'a' && firstChar <= 'z') ||
(firstChar >= '0' && firstChar <= '9')) {
return false
}
// 检查所有字符
for _, r := range name {
if !(r >= 'a' && r <= 'z') &&
!(r >= '0' && r <= '9') &&
r != '_' &&
r != '-' &&
r != '.' {
return false
}
}
// 禁止的特殊序列
forbiddenSequences := []string{"..", "__", "--", ".-", "-."}
for _, seq := range forbiddenSequences {
if strings.Contains(name, seq) {
return false
}
}
return true
}
// ExtractContainerPorts 从 DevContainerConfiguration 的 ForwardPorts 中提取容器端口
func (c *DevContainerConfiguration) ExtractContainerPorts() []int {
var containerPorts []int
for _, port := range c.ForwardPorts {
switch p := port.(type) {
case int:
// 如果是整数,既是主机端口也是容器端口
if p > 0 && p <= 65535 {
containerPorts = append(containerPorts, p)
}
case string:
// 解析端口映射格式 "hostPort:containerPort" 或单一端口
parts := strings.Split(p, ":")
if len(parts) == 1 {
// 单一端口,既是主机端口也是容器端口
if portNum, err := strconv.Atoi(parts[0]); err == nil && portNum > 0 && portNum <= 65535 {
containerPorts = append(containerPorts, portNum)
}
} else if len(parts) == 2 {
// 端口映射格式 "hostPort:containerPort",取容器端口(第二个部分)
if containerPort, err := strconv.Atoi(parts[1]); err == nil && containerPort > 0 && containerPort <= 65535 {
containerPorts = append(containerPorts, containerPort)
}
}
}
}
return containerPorts
}
// GetAllMountOptions 获取所有挂载选项的详细信息
func (c *DevContainerConfiguration) GetAllMountOptions() []map[string]interface{} {
var mountOptions []map[string]interface{}
for _, mount := range c.Mounts {
switch m := mount.(type) {
case string:
// 对于字符串格式,简单解析
parts := strings.Split(m, ":")
option := map[string]interface{}{
"source": parts[0],
"target": parts[1],
}
if len(parts) > 2 {
option["options"] = parts[2]
}
mountOptions = append(mountOptions, option)
case map[string]interface{}:
mountOptions = append(mountOptions, m)
}
}
return mountOptions
}
// ExtractMountFlags 将 Mounts 字段转换为 -v 参数字符串列表
func (c *DevContainerConfiguration) ExtractMountFlags() []string {
var mountFlags []string
// 遍历 Mounts 列表中的每个挂载项
for _, mount := range c.Mounts {
switch m := mount.(type) {
case string:
// 字符串格式的挂载,直接转换为 -v 格式
mountFlags = append(mountFlags, fmt.Sprintf("-v %s", m))
case map[string]interface{}:
// 对象格式的挂载,需要解析并转换
if flag := convertMountObjectToFlag(m); flag != "" {
mountFlags = append(mountFlags, fmt.Sprintf("-v %s", flag))
}
}
}
return mountFlags
}
// convertMountObjectToFlag 将挂载对象转换为 -v 标志字符串
func convertMountObjectToFlag(mount map[string]interface{}) string {
// 提取必需的 source 和 target 字段
source, sourceOk := mount["source"].(string)
target, targetOk := mount["target"].(string)
if !sourceOk || !targetOk {
return ""
}
// 构建基本挂载字符串
mountStr := fmt.Sprintf("%s:%s", source, target)
// 添加可选的只读标志
if readOnly, ok := mount["readOnly"].(bool); ok && readOnly {
mountStr = fmt.Sprintf("%s:ro", mountStr)
}
// 添加可选的 consistency 字段(如果存在)
if consistency, ok := mount["consistency"].(string); ok {
switch consistency {
case "consistent", "cached", "delegated":
mountStr = fmt.Sprintf("%s:%s", mountStr, consistency)
}
}
return mountStr
}
// ValidateRunArgs 验证 RunArgs 字段中的参数是否有效
func (c *DevContainerConfiguration) ValidateRunArgs() []error {
var errors []error
// 常见的有效 Docker 运行参数前缀
validArgPrefixes := []string{
"--network=", "--net=",
"--ipc=",
"--pid=",
"--user=", "-u ",
"--group-add=",
"--ulimit=",
"--sysctl=",
"--tmpfs=",
"--volume=", "-v ",
"--mount=",
"--device=",
"--cap-add=",
"--cap-drop=",
"--security-opt=",
"--env=", "-e ",
"--env-file=",
"--hostname=", "-h ",
"--domainname=",
"--mac-address=",
"--memory=", "-m ",
"--memory-swap=",
"--memory-swappiness=",
"--oom-kill-disable",
"--oom-score-adj=",
"--restart=",
"--cpu-shares=",
"--cpu-period=",
"--cpu-quota=",
"--cpuset-cpus=",
"--cpuset-mems=",
"--blkio-weight=",
"--blkio-weight-device=",
"--device-read-bps=",
"--device-write-bps=",
"--device-read-iops=",
"--device-write-iops=",
"--kernel-memory=",
"--memory-reservation=",
"--pids-limit=",
"--publish=", "-p ",
"--publish-all=", "-P",
"--expose=",
"--link=",
"--dns=",
"--dns-option=",
"--dns-search=",
"--add-host=",
"--cgroup-parent=",
"--init",
"--init-path=",
"--isolation=",
"--label=", "-l ",
"--label-file=",
"--log-driver=",
"--log-opt=",
"--name=",
"--workdir=", "-w ",
}
for i, arg := range c.RunArgs {
// 检查参数是否以有效的前缀开头
isValid := false
for _, prefix := range validArgPrefixes {
if strings.HasPrefix(arg, prefix) {
isValid = true
break
}
}
// 一些独立的标志参数
standaloneFlags := []string{
"--init", "-i", "-t", "-it", "--privileged",
"--read-only", "--rm", "--sig-proxy", "--tty",
"--interactive", "--detach", "-d",
}
for _, flag := range standaloneFlags {
if arg == flag {
isValid = true
break
}
}
if !isValid && !strings.HasPrefix(arg, "-") {
// 可能是值而不是参数,这取决于上下文
// 这里我们允许非参数开头的值
isValid = true
}
if !isValid {
errors = append(errors, fmt.Errorf("runArgs[%d]: invalid docker argument '%s'", i, arg))
}
}
return errors
}
// parseCommand 解析不同格式的命令
func (c *DevContainerConfiguration) ParseCommand(command interface{}) []string {
var commands []string
switch cmd := command.(type) {
case string:
if cmd != "" {
commands = append(commands, cmd)
}
case []interface{}:
var cmdParts []string
for _, part := range cmd {
if str, ok := part.(string); ok {
cmdParts = append(cmdParts, str)
} else {
return nil
}
}
if len(cmdParts) > 0 {
commands = cmdParts
}
case map[string]interface{}:
// 处理对象格式的命令
if commandField, ok := cmd["command"]; ok {
subCommands := c.ParseCommand(commandField)
commands = append(commands, subCommands...)
}
default:
return nil
}
return commands
}

repo.diff.view_file

@@ -0,0 +1,58 @@
package devcontainer
import devcontainer_models "code.gitea.io/gitea/models/devcontainer"
type UpdateInfo struct {
ImageName string `json:"ImageName"`
PassWord string `json:"RepositoryPassword"`
RepositoryAddress string `json:"RepositoryAddress"`
RepositoryUsername string `json:"RepositoryUsername"`
SaveMethod string `json:"SaveMethod"`
}
type OutputResponse struct {
CurrentJob struct {
IP string `json:"ip"`
Port string `json:"port"`
Title string `json:"title"`
Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"`
} `json:"currentDevcontainer"`
}
type ViewJobStep struct {
Summary string `json:"summary"`
Duration string `json:"duration"`
Status string `json:"status"`
Logs []ViewStepLogLine `json:"logs"`
}
type ViewStepLogLine struct {
Index int64 `json:"index"`
Message string `json:"message"`
Timestamp float64 `json:"timestamp"`
}
type DevcontainerList struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
DevContainers []devcontainer_models.Devcontainer `json:"devContainers"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
PageTotalNum int `json:"pageTotalNum"`
ItemTotalNum int64 `json:"itemTotalNum"`
}
type DevcontainerVO struct {
DevContainerId int64 `json:"devContainerId" xorm:"devcontainer_id"`
DevContainerName string `json:"devContainerName" xorm:"devcontainer_name"`
DevContainerHost string `json:"devContainerHost" xorm:"devcontainer_host"`
DevContainerUsername string `json:"devContainerUsername" xorm:"devcontainer_username"`
DevContainerWorkDir string `json:"devContainerWorkDir" xorm:"devcontainer_work_dir"`
UserId int64 `json:"userId" xorm:"user_id"`
RepoId int64 `json:"repoId" xorm:"repo_id"`
RepoName string `json:"repoName" xorm:"repo_name"`
//RepoOwnerID int64 `json:"repo_owner_id" xorm:"repo_owner_id"`
RepoOwnerName string `json:"repo_owner_name" xorm:"repo_owner_name"`
RepoLink string `json:"repo_link" xorm:"repo_link"`
RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"`
DevContainerPort uint16 `json:"devContainerPort,omitempty"`
DevContainerStatus uint16 `json:"devContainerStatus,omitempty" xorm:"devcontainer_status"`
}

repo.diff.view_file

@@ -0,0 +1,235 @@
package devcontainer
import (
"archive/tar"
"context"
"fmt"
"io"
"net"
"net/url"
"regexp"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/google/uuid"
)
func IsAdmin(ctx context.Context, doer *user.User, repoID int64) (bool, error) {
if doer.IsAdmin {
return true, nil
}
e := db.GetEngine(ctx)
teamMember, err := e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ",
repoID, perm.AccessModeAdmin).
And("team_user.uid = ?", doer.ID).Exist()
if err != nil {
return false, nil
}
if teamMember {
return true, nil
}
return e.Get(&repo.Collaboration{RepoID: repoID, UserID: doer.ID, Mode: 3})
}
func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (string, error) {
var err error
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", err
}
// 1. 获取默认分支名
branchName := repo.DefaultBranch
if len(branchName) == 0 {
branchName = cfg.Section("repository").Key("DEFAULT_BRANCH").Value()
}
// 2. 打开默认分支
gitRepoEntity, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return "", err
}
defer func(gitRepoEntity *git.Repository) {
_ = gitRepoEntity.Close()
}(gitRepoEntity)
// 3. 获取分支名称
commit, err := gitRepoEntity.GetBranchCommit(branchName)
if err != nil {
return "", err
}
entry, err := commit.GetTreeEntryByPath(path)
if err != nil {
return "", err
}
// No way to edit a directory online.
if entry.IsDir() {
return "", fmt.Errorf("%s entry.IsDir", path)
}
blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize {
return "", fmt.Errorf("%s blob.Size overflow", path)
}
dataRc, err := blob.DataAsync()
if err != nil {
return "", err
}
defer dataRc.Close()
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
// Only some file types are editable online as text.
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
return "", fmt.Errorf("typesniffer.IsRepresentableAsText")
}
d, _ := io.ReadAll(dataRc)
buf = append(buf, d...)
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
log.Error("ToUTF8: %v", err)
return string(buf), nil
} else {
return content, nil
}
}
// FileExists returns true if a file exists in the given repo branch
func FileExists(path string, repo *gitea_context.Repository) (bool, error) {
var branch = repo.BranchName
if branch == "" {
branch = repo.Repository.DefaultBranch
}
commit, err := repo.GitRepo.GetBranchCommit(branch)
if err != nil {
return false, err
}
if _, err := commit.GetTreeEntryByPath(path); err != nil {
return false, err
}
return true, nil
}
func ParseImageName(imageName string) (registry, namespace, repo, tag string) {
// 分离仓库地址和命名空间
parts := strings.Split(imageName, "/")
if len(parts) == 3 {
registry = parts[0]
namespace = parts[1]
repo = parts[2]
} else if len(parts) == 2 {
registry = parts[0]
repo = parts[1]
} else {
repo = imageName
}
// 分离标签
parts = strings.Split(repo, ":")
if len(parts) > 1 {
tag = parts[1]
repo = parts[0]
} else {
tag = "latest"
}
if registry == "" {
registry = "docker.io"
}
return registry, namespace, repo, tag
}
func getSanitizedDevcontainerName(username, repoName string) string {
regexpNonAlphaNum := regexp.MustCompile(`[^a-zA-Z0-9]`)
sanitizedUsername := regexpNonAlphaNum.ReplaceAllString(username, "")
sanitizedRepoName := regexpNonAlphaNum.ReplaceAllString(repoName, "")
if len(sanitizedUsername) > 15 {
sanitizedUsername = strings.ToLower(sanitizedUsername[:15])
}
if len(sanitizedRepoName) > 31 {
sanitizedRepoName = strings.ToLower(sanitizedRepoName[:31])
}
newUUID, _ := uuid.NewUUID()
uuidStr := newUUID.String()
uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")[:15]
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
}
func ReplacePortOfUrl(originalURL, targetPort string) (string, error) {
// 解析原始 URL
parsedURL, _ := url.Parse(originalURL)
// 获取主机名和端口号
host, _, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
// 如果没有端口号,则 SplitHostPort 会返回错误
// 这种情况下Host 就是主机名
host = parsedURL.Host
}
// 重新组装 Host 和端口
parsedURL.Host = net.JoinHostPort(host, targetPort)
// 生成新的 URL 字符串
newURL := parsedURL.String()
return newURL, nil
}
func GetPortFromURL(urlStr string) (string, error) {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return "", fmt.Errorf("解析URL失败: %v", err)
}
// 获取主机名和端口号
_, port, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
// 如果SplitHostPort失败说明URL中没有明确指定端口
// 需要根据协议判断默认端口
switch parsedURL.Scheme {
case "http":
return "80", nil
case "https":
return "443", nil
default:
return "", fmt.Errorf("未知协议: %s", parsedURL.Scheme)
}
}
// 如果端口存在,直接返回
return port, nil
}
// addFileToTar 将文件添加到 tar 归档
func AddFileToTar(tw *tar.Writer, filename string, content string, mode int64) error {
hdr := &tar.Header{
Name: filename,
Mode: mode,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write([]byte(content)); err != nil {
return err
}
return nil
}

repo.diff.view_file

@@ -0,0 +1,596 @@
package devcontainer
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os/exec"
"regexp"
"strings"
"time"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
)
func GetDevContainerStatusFromDocker(ctx context.Context, containerName string) (string, error) {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", err
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
return "", err
}
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
if err != nil {
return "", err
}
return containerStatus, nil
}
func CreateDevContainerByDockerAPI(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, imageName string, repo *repo.Repository, publicKeyList []string) error {
dbEngine := db.GetEngine(ctx)
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return err
}
dockerSocket, err := docker_module.GetDockerSocketPath()
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 1})
if err != nil {
log.Info("err %v", err)
}
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
script := "docker " + "-H " + dockerSocket + " pull " + configurationModel.Image
cmd := exec.Command("sh", "-c", script)
err = cmd.Start()
if err != nil {
return err
}
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 2})
if err != nil {
return err
}
docker_module.CreateAndStartContainer(ctx, cli, imageName,
[]string{
"sh",
"-c",
"tail -f /dev/null;",
},
nil,
nil,
nat.PortSet{
nat.Port("22/tcp"): {},
},
newDevcontainer.Name)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 3})
if err != nil {
return err
}
log.Info("ExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainer")
output, err := docker_module.ExecCommandInContainer(ctx, cli, newDevcontainer.Name,
`echo "`+newDevcontainer.DevcontainerHost+` host.docker.internal" | tee -a /etc/hosts;apt update;apt install -y git ;git clone `+strings.TrimSuffix(setting.AppURL, "/")+repo.Link()+" "+newDevcontainer.DevcontainerWorkDir+"/"+repo.Name+`; apt install -y ssh;echo "PubkeyAuthentication yes `+"\n"+`PermitRootLogin yes `+"\n"+`" | tee -a /etc/ssh/sshd_config;rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart;mkdir -p ~/.ssh;chmod 700 ~/.ssh;echo "`+strings.Join(publicKeyList, "\n")+`" > ~/.ssh/authorized_keys;chmod 600 ~/.ssh/authorized_keys;`,
)
if err != nil {
return err
}
log.Info(output)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
if err != nil {
return err
}
return nil
}
func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, repo *repo.Repository, publicKeyList []string) (string, error) {
dbEngine := db.GetEngine(ctx)
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return "", err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return "", err
}
var imageName = configurationModel.Image
dockerSocket, err := docker_module.GetDockerSocketPath()
if err != nil {
return "", err
}
if configurationModel.Build != nil && configurationModel.Build.Dockerfile != "" {
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
if err != nil {
return "", err
}
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
return "", err
}
if _, err := tw.Write(content); err != nil {
return "", err
}
// 执行镜像构建
imageName = fmt.Sprintf("%d", newDevcontainer.UserId) + "-" + fmt.Sprintf("%d", newDevcontainer.RepoId) + "-dockerfile"
buildOptions := types.ImageBuildOptions{
Tags: []string{imageName}, // 镜像标签
}
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", err
}
buildResponse, err := cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
return "", err
}
output, err := io.ReadAll(buildResponse.Body)
if err != nil {
return "", err
}
log.Info(string(output))
}
// 拉取镜像的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 1,
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: "docker " + "-H " + dockerSocket + " pull " + imageName + "\n",
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return imageName, err
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return imageName, err
}
var startCommand string = `docker -H ` + dockerSocket + ` run --restart=always -d --name ` + newDevcontainer.Name
// 将每个端口转换为 "-p <port>" 格式
var portFlags string = " -p 22 "
exposedPorts := configurationModel.ExtractContainerPorts()
for _, port := range exposedPorts {
portFlags = portFlags + fmt.Sprintf(" -p %d ", port)
}
startCommand += portFlags
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `"` +
` -e PublicKeyList="` + strings.Join(publicKeyList, "\n") + `" `
// 遍历 ContainerEnv 映射中的每个环境变量
for name, value := range configurationModel.ContainerEnv {
// 将每个环境变量转换为 "-e name=value" 格式
envFlags = envFlags + fmt.Sprintf(" -e %s=\"%s\" ", name, value)
}
startCommand += envFlags
if configurationModel.Init {
startCommand += " --init "
}
if configurationModel.Privileged {
startCommand += " --privileged "
}
var capAddFlags string
// 遍历 CapAdd 列表中的每个能力
for _, capability := range configurationModel.CapAdd {
// 将每个能力转换为 --cap-add=capability 格式
capAddFlags = capAddFlags + fmt.Sprintf(" --cap-add %s ", capability)
}
startCommand += capAddFlags
var securityOptFlags string
// 遍历 SecurityOpt 列表中的每个安全选项
for _, option := range configurationModel.SecurityOpt {
// 将每个选项转换为 --security-opt=option 格式
securityOptFlags = securityOptFlags + fmt.Sprintf(" --security-opt %s ", option)
}
startCommand += securityOptFlags
startCommand += " " + strings.Join(configurationModel.ExtractMountFlags(), " ") + " "
if configurationModel.WorkspaceFolder != "" {
startCommand += fmt.Sprintf(" -w %s ", configurationModel.WorkspaceFolder)
}
startCommand += " " + strings.Join(configurationModel.RunArgs, " ") + " "
//创建并运行容器的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: startCommand + imageName + ` sh -c "tail -f /dev/null"` + "\n",
ListId: 2,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return imageName, err
}
//安装基本工具的命令
onCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.OnCreateCommand), ";"))
if !strings.HasSuffix(onCreateCommand, ";") {
onCreateCommand += ";"
}
if onCreateCommand == ";" {
onCreateCommand = ""
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: `docker -H ` + dockerSocket + ` exec ` + newDevcontainer.Name + ` /home/webTerminal.sh start; ` +
onCreateCommand + "\n",
ListId: 3,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return imageName, err
}
//连接容器的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo 'Successfully connected to the container';bash"` + "\n",
ListId: 4,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return imageName, err
}
return imageName, nil
}
func IsContainerNotFound(ctx context.Context, containerName string) (bool, error) {
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
// 检查是否为 "未找到" 错误
if errdefs.IsNotFound(err) {
return true, nil
}
return false, err
}
_, err = cli.ContainerInspect(ctx, containerID)
if err != nil {
// 检查是否为 "未找到" 错误
if docker_module.IsContainerNotFound(err) {
return true, nil
}
// 其他类型的错误
return false, err
}
// 无错误表示容器存在
return false, nil
}
func DeleteDevContainerByDocker(ctx context.Context, devContainerName string) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerName)
if err != nil {
if errdefs.IsNotFound(err) {
return nil
}
return err
}
// 删除容器
if err := docker_module.DeleteContainer(ctx, cli, containerID); err != nil {
return err
}
return nil
}
func RestartDevContainerByDocker(ctx context.Context, devContainerName string) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerName)
if err != nil {
return err
}
// restart容器
timeout := 10 // 超时时间(秒)
err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return err
} else {
log.Info("容器已重启")
}
return nil
}
func StopDevContainerByDocker(ctx context.Context, devContainerName string) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerName)
if err != nil {
return err
}
// stop容器
timeout := 10 // 超时时间(秒)
err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return err
} else {
log.Info("容器已停止")
}
return nil
}
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *repo.Repository, doer *user.User) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// update容器
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return err
}
if updateInfo.SaveMethod == "on" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
if err != nil {
return err
}
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
return err
}
if _, err := tw.Write(content); err != nil {
return err
}
buildOptions := types.ImageBuildOptions{
Tags: []string{imageRef}, // 镜像标签
}
_, err = cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
log.Info(err.Error())
return err
}
} else {
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
if err != nil {
return err
}
// 提交容器
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
if err != nil {
return err
}
}
// 推送到仓库
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return err
}
err = docker_module.PushImage(dockerHost, updateInfo.RepositoryUsername, updateInfo.PassWord, updateInfo.RepositoryAddress, imageRef)
if err != nil {
return err
}
// 定义正则表达式来匹配 image 字段
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
// 使用正则表达式查找并替换 image 字段的值
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer)
if err != nil {
return err
}
return nil
}
// ImageExists 检查指定镜像是否存在
// 返回值:
// - bool: 镜像是否存在true=存在false=不存在)
// - error: 非空表示检查过程中发生错误
func ImageExists(ctx context.Context, imageName string) (bool, error) {
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err // 其他错误
}
// 获取镜像信息
_, _, err = cli.ImageInspectWithRaw(ctx, imageName)
if err != nil {
if client.IsErrNotFound(err) {
return false, nil // 镜像不存在,但不是错误
}
return false, err // 其他错误
}
return true, nil // 镜像存在
}
func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string) (bool, error) {
// 上下文
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
return false, err
}
// 创建 exec 配置
execConfig := types.ExecConfig{
Cmd: []string{"test", "-d", dirPath}, // 检查目录是否存在
AttachStdout: true,
AttachStderr: true,
}
// 创建 exec 实例
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
if err != nil {
return false, err
}
// 执行命令
var exitCode int
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
if err != nil {
return false, err
}
// 获取命令执行结果
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
if err != nil {
return false, err
}
exitCode = resp.ExitCode
return exitCode == 0, nil // 退出码为 0 表示目录存在
}
func RegistWebTerminal(ctx context.Context) error {
log.Info("开始构建WebTerminal...")
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
//拉取web_terminal镜像
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return fmt.Errorf("获取docker socket路径失败:%v", err)
}
// 拉取镜像
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
if err != nil {
return fmt.Errorf("拉取web_terminal镜像失败:%v", err)
}
timestamp := time.Now().Format("20060102150405")
binds := []string{
"/var/run/docker.sock:/var/run/docker.sock",
}
containerName := "webterminal-" + timestamp
//创建并启动WebTerminal容器
err = docker_module.CreateAndStartContainer(ctx, cli, setting.DevContainerConfig.Web_Terminal_Image,
nil,
nil, binds,
nat.PortSet{
"7681/tcp": struct{}{},
},
containerName)
if err != nil {
return fmt.Errorf("创建并注册WebTerminal失败:%v", err)
}
// Save settings.
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = docker_module.GetMappedPort(ctx, containerName, "7681")
if err != nil {
return err
}
cfg.Section("devcontainer").Key("WEB_TERMINAL_CONTAINER").SetValue(containerName)
if err = cfg.SaveTo(setting.CustomConf); err != nil {
return err
}
return nil
}

repo.diff.view_file

@@ -49,6 +49,7 @@ type AdminEditUserForm struct {
AllowGitHook bool
AllowImportLocal bool
AllowCreateOrganization bool
AllowCreateDevcontainer bool
ProhibitLogin bool
Reset2FA bool `form:"reset_2fa"`
Visibility structs.VisibleType

repo.diff.view_file

@@ -0,0 +1,21 @@
package forms
import (
"net/http"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
// CreateRepoDevcontainerForm 用户使用 API 创建仓库 DevContainer POST 表单数据绑定与校验规则
type CreateRepoDevcontainerForm struct {
RepoId string `json:"repoId" binding:"Required;MinSize(1);MaxSize(19);PositiveBase10IntegerNumberRule"`
SSHPublicKeyList []string `json:"sshPublicKeyList"`
}
// Validate 用户使用 API 创建仓库 DevContainer POST 表单数据校验器
func (f *CreateRepoDevcontainerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

repo.diff.view_file

@@ -66,7 +66,7 @@ func registDockerRunner(ctx context.Context, token string) error {
return fmt.Errorf("获取docker socket路径失败:%v", err)
}
// 拉取镜像
err = docker_module.PullImage(cli, dockerHost, setting.Runner.Image)
err = docker_module.PullImage(ctx, cli, dockerHost, setting.Runner.Image)
if err != nil {
return fmt.Errorf("拉取act_runner镜像失败:%v", err)
}
@@ -91,7 +91,7 @@ func registDockerRunner(ctx context.Context, token string) error {
}
containerName := "runner-" + timestamp
//创建并启动Runner容器
err = docker_module.CreateAndStartContainer(cli, setting.Runner.Image, nil, env, binds, nil, containerName)
err = docker_module.CreateAndStartContainer(ctx, cli, setting.Runner.Image, nil, env, binds, nil, containerName)
if err != nil {
return fmt.Errorf("创建并注册Runner失败:%v", err)
}
@@ -123,7 +123,7 @@ func deleteDockerRunnerByName(ctx context.Context, runnerName string) error {
}
log.Info("[StopAndRemoveContainer]Docker client创建成功")
defer cli.Close()
err = docker_module.DeleteContainer(cli, runnerName)
err = docker_module.DeleteContainer(ctx, cli, runnerName)
if err != nil {
return fmt.Errorf("Runner创建失败:%v", err)
}

repo.diff.view_file

@@ -51,6 +51,7 @@ type UpdateOptions struct {
Theme optional.Option[string]
DiffViewStyle optional.Option[string]
AllowCreateOrganization optional.Option[bool]
AllowCreateDevcontainer optional.Option[bool]
IsActive optional.Option[bool]
IsAdmin optional.Option[UpdateOptionField[bool]]
EmailNotificationsPreference optional.Option[string]
@@ -164,6 +165,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
cols = append(cols, "allow_create_organization")
}
if opts.AllowCreateDevcontainer.Has() {
u.AllowCreateDevcontainer = opts.AllowCreateDevcontainer.Value()
cols = append(cols, "allow_create_devcontainer")
}
if opts.RepoAdminChangeTeamAccess.Has() {
u.RepoAdminChangeTeamAccess = opts.RepoAdminChangeTeamAccess.Value()

repo.diff.view_file

@@ -148,6 +148,12 @@
</div>
</div>
{{end}}
<div class="inline field">
<div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.users.allow_create_devcontainer"}}</strong></label>
<input name="allow_create_devcontainer" type="checkbox" {{if .User.AllowCreateDevcontainer}}checked{{end}}>
</div>
</div>
{{if .TwoFactorEnabled}}
<div class="divider"></div>

repo.diff.view_file

@@ -158,9 +158,10 @@
<!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<div>
<!-- k8s -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}">
<summary class="right-content tw-py-2{{if .Err_K8s}} text red{{end}}">
{{ctx.Locale.Tr "install.k8s_title"}}
</summary>
<div class="inline field">
@@ -224,6 +225,7 @@
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}">
{{ctx.Locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
<div class="ui checkbox" id="offline-mode">
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label>

repo.diff.view_file

@@ -0,0 +1,414 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository wiki pages">
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<!-- 开始Dev Container 正文 -->
<div class="issue-content">
<!-- 开始Dev Container 正文内容 - 左侧主展示区 -->
<div class="issue-content-left">
{{if not .HasDevContainerConfiguration}}
<div class="empty-placeholder">
{{svg "octicon-container" 48}}
<h2>{{ctx.Locale.Tr "repo.dev_container_empty"}}</h2>
{{if .isAdmin}}
<form method="get" action="{{.CreateDevcontainerSettingUrl}}" class="ui edit form">
<button class="ui primary button" type="submit">Create</button>
</form>
{{end}}
</div>
{{else}}
<div class="ui container">
<form class="ui edit form">
<div class="repo-editor-header">
<div class="ui breadcrumb field">
<a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a>
{{range $i, $v := .TreeNames}}
<div class="breadcrumb-divider">/</div>
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
{{end}}
</div>
<a href="{{.EditDevcontainerConfigurationUrl}}"><div class="ui primary button" style="margin-left: 10px;width: 4em;height: 1em;">Edit</div></a>
</div>
</form>
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
</div>
{{end}}
</div>
<!-- 结束Dev Container 正文内容 - 左侧主展示区 -->
<!-- 开始Dev Container 正文内容 - 右侧展示区 -->
<div class="issue-content-right ui segment">
<strong>Options</strong>
<div class="ui relaxed list">
{{if .HasDevContainer}}
<div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/devcontainer/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
{{if .isAdmin}}
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
{{end}}
<div style=" display: none;" id="restartContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
<div style=" display: none;" id="stopContainer" class="item"><button class="flex-text-inline" style="color:black; " >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></div>
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
<div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
<div style=" display: none;" id="cursorTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
<div style=" display: none;" id="windsurfTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
{{end}}
{{if .ValidateDevContainerConfiguration}}
<div style=" display: none;" id="createContainer" class="item">
<div>
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</button>
</form>
</div>
</div>
<div id="loading" class="loading"></div>
{{end}}
{{if not .ValidateDevContainerConfiguration}}
<div class="item">{{svg "octicon-alert" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_invalid_config_prompt"}} </div>
{{end}}
</div>
</div>
<!-- 结束Dev Container 正文内容 - 右侧展示区 -->
</div>
<!-- 结束Dev Container 正文内容 -->
</div>
</div>
<!-- 确认删除 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.dev_container_control.delete"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.dev_container_control.deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
<!-- 确认 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
<div class="header">
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
</div>
<div class="content">
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
<div class="inline field">
<div class="ui checkbox">
{{if not .HasDevContainerDockerfile}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
{{else}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
{{end}}
<label for="SaveMethod">Build From Dockerfile</label>
</div>
</div>
<div class="required field ">
<label for="RepositoryAddress">Registry:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
</div>
<div class="required field ">
<label for="RepositoryUsername">Registry Username:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryUsername" name="RepositoryUsername" value="{{.RepositoryUsername}}">
</div>
<div class="required field ">
<label for="RepositoryPassword">Registry Password:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryPassword" name="RepositoryPassword" required>
</div>
<div class="required field ">
<label for="ImageName">Image(name:tag):</label>
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
</div>
<div class="actions">
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
<button class="ui cancel button" id="updateCloseButton">Close</button>
</div>
</form>
</div>
</div>
<script>
var status = '-1'
var intervalID
const createContainer = document.getElementById('createContainer');
const deleteContainer = document.getElementById('deleteContainer');
const updateContainer = document.getElementById('updateContainer');
const restartContainer = document.getElementById('restartContainer');
const stopContainer = document.getElementById('stopContainer');
const webTerminal = document.getElementById('webTerminal');
const vsTerminal = document.getElementById('vsTerminal');
const cursorTerminal = document.getElementById('cursorTerminal');
const windsurfTerminal = document.getElementById('windsurfTerminal');
const webTerminalContainer = document.getElementById('webTerminalContainer');
const loadingElement = document.getElementById('loading');
function concealElement(){
if (createContainer){
createContainer.style.display = 'none';
}
if (deleteContainer){
deleteContainer.style.display = 'none';
}
if (updateContainer) {
updateContainer.style.display = 'none';
}
if (restartContainer) {
restartContainer.style.display = 'none';
}
if (stopContainer) {
stopContainer.style.display = 'none';
}
if (webTerminal) {
webTerminal.style.display = 'none';
}
if (vsTerminal) {
vsTerminal.style.display = 'none';
}
if (cursorTerminal) {
cursorTerminal.style.display = 'none';
}
if (windsurfTerminal) {
windsurfTerminal.style.display = 'none';
}
if (webTerminalContainer) {
webTerminalContainer.style.display = 'none';
}
}
function displayElement(){
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (restartContainer) {
restartContainer.style.display = 'block';
}
if (stopContainer) {
stopContainer.style.display = 'block';
}
if (webTerminal) {
webTerminal.style.display = 'block';
}
if (vsTerminal) {
vsTerminal.style.display = 'block';
}
if (cursorTerminal) {
cursorTerminal.style.display = 'block';
}
if (windsurfTerminal) {
windsurfTerminal.style.display = 'block';
}
if (webTerminalContainer) {
webTerminalContainer.style.display = 'block';
}
}
function getStatus() {
fetch(
'{{.Repository.Link}}'+'/devcontainer/status'
)
.then(response => response.json())
.then(data => {
if (data.status == '-1' || data.status == '') {
if (loadingElement) {
loadingElement.style.display = 'none';
}
if (createContainer){
createContainer.style.display = 'block';
}
clearInterval(intervalID);
} else if (data.status == '0' || data.status == '1' || data.status == '2') {
concealElement();
if (webTerminalContainer) {
webTerminalContainer.style.display = 'block';
}
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (loadingElement) {
loadingElement.style.display = 'block';
}
}else if (data.status == '3') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (webTerminalContainer) {
webTerminalContainer.style.display = 'block';
}
if (loadingElement) {
loadingElement.style.display = 'block';
}
}else if (data.status == '4') {
displayElement();
if (loadingElement) {
loadingElement.style.display = 'none';
}
clearInterval(intervalID);
}else if (data.status == '5') {
concealElement();
if (loadingElement) {
loadingElement.style.display = 'block';
}
}else if (data.status == '6') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (loadingElement) {
loadingElement.style.display = 'block';
}
}else if (data.status == '7') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (loadingElement) {
loadingElement.style.display = 'block';
}
}else if (data.status == '8') {
concealElement();
if (deleteContainer){
deleteContainer.style.display = 'block';
}
if (updateContainer) {
updateContainer.style.display = 'block';
}
if (restartContainer) {
restartContainer.style.display = 'block';
}
if (loadingElement) {
loadingElement.style.display = 'none';
}
clearInterval(intervalID);
}else if (data.status == '9') {
concealElement();
if (loadingElement) {
loadingElement.style.display = 'block';
}
}
if(status !== '9' && status !== '-1' && data.status == '9'){
window.location.reload();
}
if(status !== '-1' && data.status == '-1'){
window.location.reload();
}
status = data.status
})
.catch(error => {
console.error('Error:', error);
});
}
intervalID = setInterval(getStatus, 3000);
if (restartContainer) {
restartContainer.addEventListener('click', function(event) {
// 处理点击逻辑
concealElement();
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
.then(response => {intervalID = setInterval(getStatus, 3000);})
});
}
if (stopContainer) {
stopContainer.addEventListener('click', function(event) {
concealElement();
// 处理点击逻辑
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
.then(response => {intervalID = setInterval(getStatus, 3000);})
});
}
if (deleteContainer) {
deleteContainer.addEventListener('click', function(event) {
setInterval(getStatus, 3000);
});
}
function submitForm(event) {
event.preventDefault(); // 阻止默认的表单提交行为
const {csrfToken} = window.config;
const {appSubUrl} = window.config;
const form = document.getElementById('updateForm');
const submitButton = document.getElementById('updateSubmitButton');
const closeButton = document.getElementById('updateCloseButton');
submitButton.disabled = true;
const formData = new FormData(form);
fetch('{{.Repository.Link}}'+'/devcontainer/update', {
method: 'POST',
headers: {
'x-csrf-token': csrfToken, // 如果需要认证
'content-type' : 'application/json',
},
body: JSON.stringify({
RepositoryAddress: formData.get('RepositoryAddress'),
RepositoryUsername: formData.get('RepositoryUsername'),
RepositoryPassword: formData.get('RepositoryPassword'),
SaveMethod: formData.get('SaveMethod'),
ImageName: formData.get('ImageName'),
})
})
.then(response => response.json())
.then(data => {
submitButton.disabled = false;
alert(data.message);
if(data.redirect){
closeButton.click()
}
intervalID = setInterval(getStatus, 3000);
})
.catch((error) => {
submitButton.disabled = false;
alert('提交失败,请重试。');
});
}
</script>
<style>
.loading{
width:60px;
height:60px;
border-radius:150px;
border:8px solid #fff;
border-top-color:rgba(0,0,0,0.3);
box-sizing:border-box;
margin-left:calc(50% - 30px);
animation:loading 1.2s linear infinite;
-webkit-animation:loading 1.2s linear infinite;
}
@keyframes loading{
0%{transform:rotate(0deg)}
100%{transform:rotate(360deg)}
}
@-webkit-keyframes loading{
0%{-webkit-transform:rotate(0deg)}
100%{-webkit-transform:rotate(360deg)}
}
</style>
{{template "base/footer" .}}

repo.diff.view_file

@@ -0,0 +1,434 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{UserThemeName .SignedUser}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title>
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
<meta name="keywords" content="{{MetaKeywords}}">
<meta name="referrer" content="no-referrer">
{{if .GoGetImport}}
<meta name="go-import" content="{{.GoGetImport}} git {{.RepoCloneLink.HTTPS}}">
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
{{end}}
{{if and .EnableFeed .FeedURL}}
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
{{end}}
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
<script>
window.config = {
appUrl: '{{AppUrl}}',
appSubUrl: '{{AppSubUrl}}',
assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly
assetUrlPrefix: '{{AssetUrlPrefix}}',
runModeIsProd: {{.RunModeIsProd}},
customEmojis: {{CustomEmojis}},
csrfToken: '{{.CsrfToken}}',
pageData: {{.PageData}},
{{if or .Participants .Assignees .MentionableTeams}}
mentionValues: Array.from(new Map([
{{- range .Participants -}}
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
{{- end -}}
{{- range .Assignees -}}
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
{{- end -}}
{{- range .MentionableTeams -}}
['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}', name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
{{- end -}}
]).values()),
{{end}}
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
{{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}}
i18n: {
copy_success: {{ctx.Locale.Tr "copy_success"}},
copy_error: {{ctx.Locale.Tr "copy_error"}},
error_occurred: {{ctx.Locale.Tr "error.occurred"}},
network_error: {{ctx.Locale.Tr "error.network_error"}},
remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
modal_cancel: {{ctx.Locale.Tr "modal.cancel"}},
more_items: {{ctx.Locale.Tr "more_items"}},
},
};
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}
window.config.pageData = window.config.pageData || {};
</script>
<script src="{{AssetUrlPrefix}}/js/webcomponents.js?v={{AssetVersion}}"></script>
<noscript>
<style>
.dropdown:hover > .menu { display: block; }
.ui.secondary.menu .dropdown.item > .menu { margin-top: 0; }
</style>
</noscript>
{{template "base/head_opengraph" .}}
{{template "base/head_style" .}}
</head>
<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
<div class="full height">
<noscript>{{ctx.Locale.Tr "enable_javascript"}}</noscript>
{{template "custom/body_inner_pre" .}}
{{$notificationUnreadCount := 0}}
{{if and .IsSigned .NotificationUnreadCount}}
{{$notificationUnreadCount = call .NotificationUnreadCount}}
{{end}}
<nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}">
<img width="auto" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
<div class="ui secondary menu item navbar-mobile-right only-mobile">
{{if .IsSigned}}
<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
<div class="tw-relative">
{{svg "octicon-bell"}}
<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
</div>
</a>
{{end}}
<button class="item tw-w-auto ui icon mini button tw-p-2 tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
</div>
<!-- navbar links non-mobile -->
{{template "custom/extra_links" .}}
</div>
<!-- the full dropdown menus -->
<div class="navbar-right">
<!-- TODO: 注册触发外部链接 -->
<!-- <a id="signup-link" class="item active" href="{{AppSubUrl}}/user/sign_up">-->
<!-- {{svg "octicon-person"}} {{ctx.Locale.Tr "register"}}-->
<!-- </a>-->
<a id="signin-link" class="item active" rel="nofollow" href="javascript:openLoginModal()">
{{svg "octicon-sign-in"}} {{ctx.Locale.Tr "sign_in"}}
</a>
<a id="logout-link" class="item active" href="javascript:logout()">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
</div><!-- end full right menu -->
</nav>
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}" class="page-content home">
<div class="ui middle very relaxed page">
<!-- login -->
<div id="loginModal" class="ui modal" style=" width:50%; left: 25%; top: 20%">
<div class="content">
<div class="close right" style="float: right" onclick="closeLoginModal()">X</div>
<div class="ui container column fluid">
<h2 class="ui top attached header center i18n" data-key="login_modal"></h2>
<div class="ui attached segment">
<form id="loginForm" class="ui form" action="javascript:login()">
<div class="required field">
<label for="username" class="i18n" data-key="username"></label>
<input type="text" id="username" name="username" required>
</div>
<div class="required field">
<label for="password" class="i18n" data-key="password"></label>
<input type="password" id="password" name="password" required>
</div>
<button class="ui button primary center i18n" data-key="login_button"></button>
</form>
</div>
</div>
</div>
</div>
<div class="column">
<div id="contentBeforeSigned" class="ui container" style="display: none">
<h3 class="header center i18n" data-key="login_reminder"></h3>
</div>
<div id="contentAfterSigned" class="ui container " style="display: none">
<button class="ui button primary i18n" data-key="createNewRepositoryButton" onclick="openModalForCreateRepository()"></button>
<div class="divider"></div>
<!-- 仓库列表 -->
<div id="repo_list" class="flex-list">
<!-- 动态加载-->
</div>
<!-- ====================== Created Repository ==========================-->
<div id="modalForCreatingRepo" class="ui modal" style="width:70%; left: 15%; top: 20%">
<div class="content">
<div class="close right" style="float: right" onclick="closeModalForCreatingRepo()">X</div>
<div class="ui container column fluid" style="height: 50%;">
<h2 class="ui top attached header center i18n" data-key="createNewRepositoryModal"></h2>
<div class="ui attached segment" >
<form id="creatingRepoForm" class="ui form" onsubmit="return createRepo()">
<!-- repo name -->
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
<span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
</div>
<!-- visibility -->
<!-- <div class="inline field">-->
<!-- <label>{{ctx.Locale.Tr "repo.visibility"}}</label>-->
<!-- <div class="ui checkbox">-->
<!-- <input id="is_private" name="private" type="checkbox" >-->
<!-- <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>-->
<!-- </div>-->
<!-- <span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>-->
<!-- </div>-->
<!-- desc -->
<div class="inline field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
<textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea>
</div>
<!-- template -->
<div id="devstar_template_area" class="inline field">
<label>{{ctx.Locale.Tr "repo.devstar_template"}}</label>
<div id="devstar_template_search" class="ui search selection dropdown">
<input type="hidden" id="devstar_template" name="devstar_template" value="">
<div id="devstar_template_name" class="default text">{{.devstar_template_name}}</div>
<div class="menu">
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.devstar_template_desc" "https://DevStar.cn/"}}</span>
</div>
<div id="git_url_template_area" class="inline field">
<label>{{ctx.Locale.Tr "repo.git_url_template"}}</label>
<input id="git_url_template" name="git_url_template" value="" placeholder="{{ctx.Locale.Tr "repo.git_url_template_helper"}}">
<span class="help">{{ctx.Locale.Tr "repo.git_url_template_desc"}}</span>
</div>
<div id="repo_template_area">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div id="repo_template_search" class="ui search selection dropdown">
<input type="hidden" id="repo_template" name="repo_template" value="">
<div id="repo_template_name" class="default text">{{.devstar_template_name}}</div>
<div class="menu">
</div>
</div>
</div>
<div id="template_units" {{if not .repo_template}}class="tw-hidden"{{end}}>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
<div class="ui checkbox">
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_content"}}</label>
</div>
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label>
</div>
<div class="ui checkbox">
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.topics"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.avatar"}}</label>
</div>
<div class="ui checkbox">
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label>
</div>
</div>
</div>
</div>
<div id="non_template" {{if .repo_template}}class="tw-hidden"{{end}}>
<!-- issue label -->
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.issue_labels"}}</label>
<div class="ui search selection dropdown">
<input id="issue_label" type="hidden" name="issue_labels" value="{{.issueLabels}}">
<div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
<div id="issue_label_menu" class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
</div>
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<label>.gitignore</label>
<div class="ui multiple search selection dropdown">
<input id="gitignore" type="hidden" name="gitignores" value="{{.gitignores}}">
<div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div>
<div id="gitignore_menu" class="menu">
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.license"}}</label>
<div class="ui search selection dropdown">
<input id="license" type="hidden" name="license" value="{{.license}}">
<div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div>
<div id="license_menu" class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div>
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.readme"}}</label>
<div class="ui selection dropdown">
<input id="readme" type="hidden" name="readme" value="{{.readme}}">
<div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div>
<div id="readme_menu" class="menu">
{{range .Readmes}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span>
</div>
<div class="inline field">
<label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label>
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.object_format"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required>
<div id="default_object_format_name" class="default text">{{.DefaultObjectFormat.Name}}</div>
<div id="object_format_menu" class="menu">
{{range .SupportedObjectFormats}}
<div class="item" data-value="{{.Name}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div class="ui checkbox">
<input id="is_template" name="template" type="checkbox">
<label>{{ctx.Locale.Tr "repo.template_helper"}}</label>
</div>
</div>
</div>
<br>
<div class="inline field">
<label></label>
<button class="ui primary button" >
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<!-- footer start -->
{{if false}}
{{/* to make html structure "likely" complete to prevent IDE warnings */}}
<html>
<body>
<div>
{{end}}
{{template "custom/body_inner_post" .}}
</div>
{{template "custom/body_outer_post" .}}
<!-- content from {{template "base/footer_content" .}} -->
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
<div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}">
{{if (or .ShowFooterVersion .PageIsAdmin)}}
{{ctx.Locale.Tr "version"}}:
{{if .IsAdmin}}
<a href="{{AppSubUrl}}/admin/config">{{AppVer}}</a>
{{else}}
{{AppVer}}
{{end}}
{{end}}
{{if and .TemplateLoadTimes ShowFooterTemplateLoadTime}}
{{ctx.Locale.Tr "page"}}: <strong>{{LoadTimes .PageStartTime}}</strong>
{{ctx.Locale.Tr "template"}}{{if .TemplateName}} {{.TemplateName}}{{end}}: <strong>{{call .TemplateLoadTimes}}</strong>
{{end}}
<p> &copy; 2025 <a target="_blank" rel="noopener noreferrer" href="https://www.mengning.com.cn">Mengning Software</a>. All rights reserved.</p>
</div>
<div class="right-links" role="group" aria-label="{{ctx.Locale.Tr "aria.footer.links"}}">
<!-- 不支持切换语言 -->
<div class="ui dropdown upward language">
<span class="flex-text-inline">{{svg "octicon-globe" 14}} {{ctx.Locale.LangName}}</span>
<!-- <div class="menu language-menu">
{{range .AllLangs}}
<a lang="{{.Lang}}" data-url="{{AppSubUrl}}/?lang={{.Lang}}" class="item {{if eq ctx.Locale.Lang .Lang}}active selected{{end}}">{{.Name}}</a>
{{end}}
</div> -->
</div>
<a href="{{AssetUrlPrefix}}/licenses.txt">{{ctx.Locale.Tr "licenses"}}</a>
{{if ShowFooterPoweredBy}}
<a target="_blank" rel="noopener noreferrer" href="https://github.com/mengning/DevStar">{{ctx.Locale.Tr "powered_by" "DevStar"}}</a>
{{end}}
{{if .EnableSwagger}}<a id="apiswagger" href="{{AppSubUrl}}/api/swagger">API</a>{{end}}
<!-- {{template "custom/extra_links_footer" .}} -->
<a id="beian" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">苏ICP备2024068144号-3</a>
</div>
</footer>
<script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script>
{{template "custom/footer" .}}
</body>
</html>
<!-- footer end -->
{{template "repo/devcontainer/vscode-home-js" .}}

repo.diff.view_file

@@ -141,6 +141,15 @@
</a>
{{end}}
<!-- 定义DevContainer tab -->
{{if .AllowCreateDevcontainer }}
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
<a class="{{if .PageIsDevContainer}}active {{end}}item" href="{{.RepoLink}}/devcontainer">
{{svg "octicon-container"}} {{ctx.Locale.Tr "repo.dev_container"}}
</a>
{{end}}
{{end}}
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}}
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues">
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}

70
webTerminal.sh Normal file
repo.diff.view_file

@@ -0,0 +1,70 @@
#!/bin/bash
# 获取参数
ACTION=$1
OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
# 根据参数执行不同命令
case $ACTION in
"start")
echo "Starting service..."
# 启动服务的命令
echo "$DevstarHost host.docker.internal" | tee -a /etc/hosts;
case $OS_ID in
ubuntu|debian)
apt-get update -y
apt-get install ssh git -y
;;
centos)
# sudo yum update -y
# sudo yum install -y epel-release
# sudo yum groupinstall -y "Development Tools"
# sudo yum install -y yaml-cpp yaml-cpp-devel
;;
fedora)
# sudo dnf update -y
# sudo dnf group install -y "Development Tools"
# sudo dnf install -y yaml-cpp yaml-cpp-devel
;;
*)
failure "Unsupported OS: $OS_ID"
exit 1
;;
esac
echo -e "PubkeyAuthentication yes\nPermitRootLogin yes\n" | tee -a /etc/ssh/sshd_config;
rm -f /etc/ssh/ssh_host_*;
ssh-keygen -A;
mkdir -p ~/.ssh;
chmod 700 ~/.ssh;
case $OS_ID in
ubuntu|debian)
service ssh restart;
;;
centos)
;;
fedora)
;;
*)
failure "Unsupported OS: $OS_ID"
exit 1
;;
esac
echo "$PublicKeyList" > ~/.ssh/authorized_keys;
chmod 600 ~/.ssh/authorized_keys
git clone $RepoLink $WorkSpace
;;
"stop")
echo "Stopping service..."
# 停止服务的命令
;;
"restart")
echo "Restarting service..."
# 重启服务的命令
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac