Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfdd2713d3 | ||
|
|
ddb06706f3 | ||
|
|
856c9d7f2b | ||
|
|
735d131321 | ||
|
|
b2b6686f46 | ||
|
|
710ec5d69a | ||
|
|
b92948cb0a | ||
|
|
8bc2b852eb | ||
|
|
4ad19d8b5c | ||
|
|
b7a8538f6e | ||
|
|
9bd0a95e9f | ||
|
|
5818970a2a | ||
|
|
00d593260e | ||
|
|
6f7e600645 | ||
|
|
f382591ef1 | ||
|
|
706b39861d | ||
|
|
c71f3eeff4 | ||
|
|
13fc6ec45d | ||
|
|
d15996c3ee | ||
|
|
b27f338d4d | ||
|
|
68c64d1716 | ||
|
|
5e359c6231 | ||
|
|
b4513b55ba | ||
|
|
1b1a4c0903 | ||
|
|
6ed635306c | ||
|
|
d0b565c94e | ||
|
|
dc0d1e0645 | ||
|
|
5e412826b6 | ||
|
|
6c3bd42839 | ||
|
|
45f4f5a6c3 | ||
|
|
3bbd048204 | ||
|
|
15bd1d61c4 | ||
|
|
b569c3f8a8 | ||
|
|
ded40c34c5 | ||
|
|
e53c8fd040 | ||
|
|
a3c1aa3011 | ||
|
|
469551095b | ||
|
|
5ca3661c33 | ||
|
|
a68d691750 | ||
| 3a1075d6a0 | |||
| 6795122e00 | |||
| 2fc3e5a1c7 | |||
| 1af82412c0 | |||
| 5832d93f0a | |||
| 44f04a7866 | |||
| 2ba34c0abb | |||
| 1717a0c45c | |||
| e871e65342 | |||
| 8b8812f81c | |||
| 67ff066157 | |||
| 3fb751bc24 | |||
| 6cfd51e4c7 | |||
| 659e08da6c | |||
| d664ce29d8 | |||
| 4580e5c87f | |||
| 11b2ee48e9 | |||
| 85d73a2d85 | |||
| 54510ce582 | |||
| 1986d90df0 | |||
| 5b0442d357 | |||
| d44fea18d5 | |||
| e57b4f1654 | |||
| 69d7c72ba8 | |||
|
|
919746c756 | ||
|
|
853ff29ae2 | ||
|
|
7292421334 | ||
|
|
84adad19bf | ||
|
|
01c9563d1d | ||
| c7a7d8cd67 | |||
| d3bf936570 |
@@ -22,7 +22,7 @@ jobs:
|
||||
# Lint job - must pass
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# Unit tests with SQLite (no external database needed)
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
# Integration tests with PostgreSQL
|
||||
test-pgsql:
|
||||
name: Integration Tests (PostgreSQL)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: linux-latest
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:15
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Build test binary
|
||||
run: |
|
||||
go build -tags="bindata sqlite sqlite_unlock_notify" -o gitea .
|
||||
go build -tags="bindata sqlite sqlite_unlock_notify" -o gitcaddy-server .
|
||||
|
||||
- name: Generate test config
|
||||
run: |
|
||||
@@ -151,11 +151,55 @@ jobs:
|
||||
TEST_PGSQL_SCHEMA: gtestschema
|
||||
GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT: true
|
||||
|
||||
# Create release job - runs first to create the release before build jobs upload
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: linux-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
outputs:
|
||||
release_id: ${{ steps.create.outputs.release_id }}
|
||||
steps:
|
||||
- name: Create or get release
|
||||
id: create
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
echo "Creating/getting release for tag: $TAG"
|
||||
|
||||
# Try to get existing release first
|
||||
EXISTING=$(curl -sf \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases/tags/$TAG" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$EXISTING" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$EXISTING" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Found existing release: $RELEASE_ID"
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create new release
|
||||
echo "Creating new release..."
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag_name":"'"$TAG"'","name":"GitCaddy Server '"$TAG"'","body":"Official release of GitCaddy Server '"$TAG"'.","draft":false,"prerelease":false}' \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases" 2>&1)
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Created release: $RELEASE_ID"
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ERROR: Failed to create release: $RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build job for binaries
|
||||
build:
|
||||
name: Build Binaries
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint]
|
||||
runs-on: linux-latest
|
||||
needs: [lint, create-release]
|
||||
if: always() && needs.lint.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -206,7 +250,7 @@ jobs:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
|
||||
LDFLAGS="-X code.gitea.io/gitea/modules/setting.AppVer=${VERSION}"
|
||||
|
||||
EXT=""
|
||||
@@ -214,7 +258,7 @@ jobs:
|
||||
EXT=".exe"
|
||||
fi
|
||||
|
||||
OUTPUT="gitea-${VERSION}-${GOOS}-${GOARCH}${EXT}"
|
||||
OUTPUT="gitcaddy-server-${VERSION}-${GOOS}-${GOARCH}${EXT}"
|
||||
|
||||
go build -v -trimpath -tags "${TAGS}" -ldflags "${LDFLAGS}" -o "dist/${OUTPUT}" .
|
||||
|
||||
@@ -223,62 +267,42 @@ jobs:
|
||||
|
||||
- name: Upload to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
run: |
|
||||
set -e
|
||||
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev")
|
||||
echo "Uploading binaries for $VERSION"
|
||||
|
||||
# Get or create release (with retry for race conditions)
|
||||
TAG="${{ github.ref_name }}"
|
||||
for i in 1 2 3; do
|
||||
EXISTING=$(curl -sf \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/$TAG" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$EXISTING" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$EXISTING" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Found existing release: $RELEASE_ID"
|
||||
break
|
||||
else
|
||||
echo "Attempt $i: Creating release..."
|
||||
RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag_name":"'"$TAG"'","name":"GitCaddy '"$TAG"'","body":"Official release.","draft":false,"prerelease":false}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" 2>&1 || echo "")
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
echo "Created release: $RELEASE_ID"
|
||||
break
|
||||
fi
|
||||
echo "Failed to create, retrying in 5s..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
echo "Uploading binaries to release ID: $RELEASE_ID"
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "ERROR: Could not get or create release"
|
||||
echo "ERROR: No release ID provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload files
|
||||
# Upload files with retry
|
||||
for file in dist/*; do
|
||||
if [ -f "$file" ]; then
|
||||
filename=$(basename "$file")
|
||||
echo "Uploading $filename..."
|
||||
|
||||
UPLOAD_RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@$file" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=$filename" 2>&1)
|
||||
for attempt in 1 2 3; do
|
||||
UPLOAD_RESPONSE=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@$file" \
|
||||
"https://direct.git.marketally.com/api/v1/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=$filename" 2>&1 || echo "")
|
||||
|
||||
if echo "$UPLOAD_RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
echo "✓ Uploaded $filename successfully"
|
||||
else
|
||||
echo "✗ Failed to upload $filename: $UPLOAD_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
if echo "$UPLOAD_RESPONSE" | grep -q '"id":[0-9]'; then
|
||||
echo "✓ Uploaded $filename successfully"
|
||||
break
|
||||
else
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "Attempt $attempt failed, retrying in 5s..."
|
||||
sleep 5
|
||||
else
|
||||
echo "✗ Failed to upload $filename after 3 attempts: $UPLOAD_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
echo "All uploads complete!"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -85,7 +85,6 @@ cpu.out
|
||||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
/public/assets/licenses.txt
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
|
||||
6
Makefile
6
Makefile
@@ -41,7 +41,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_IMAGE ?= gitcaddy/gitcaddy-server
|
||||
DOCKER_TAG ?= latest
|
||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
|
||||
@@ -70,10 +70,10 @@ else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
|
||||
endif
|
||||
ifeq ($(IS_WINDOWS),yes)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= gitea.exe
|
||||
EXECUTABLE ?= gitcaddy-server.exe
|
||||
else
|
||||
GOFLAGS := -v
|
||||
EXECUTABLE ?= gitea
|
||||
EXECUTABLE ?= gitcaddy-server
|
||||
endif
|
||||
|
||||
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
||||
|
||||
11
README.md
11
README.md
@@ -260,11 +260,11 @@ autoUpdater.checkForUpdates()
|
||||
|
||||
### From Binary
|
||||
|
||||
Download from [Releases](https://git.marketally.com/gitcaddy/gitea/releases):
|
||||
Download from [Releases](https://git.marketally.com/gitcaddy/gitcaddy-server/releases):
|
||||
|
||||
```bash
|
||||
# Linux (amd64)
|
||||
curl -L -o gitcaddy https://git.marketally.com/gitcaddy/gitea/releases/latest/download/gitea-linux-amd64
|
||||
curl -L -o gitcaddy-server https://git.marketally.com/gitcaddy/gitcaddy-server/releases/latest/download/gitcaddy-server-linux-amd64
|
||||
chmod +x gitcaddy
|
||||
./gitcaddy web
|
||||
```
|
||||
@@ -272,10 +272,10 @@ chmod +x gitcaddy
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://git.marketally.com/gitcaddy/gitea.git
|
||||
cd gitea
|
||||
git clone https://git.marketally.com/gitcaddy/gitcaddy-server.git
|
||||
cd gitcaddy-server
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
./gitea web
|
||||
./gitcaddy-server web
|
||||
```
|
||||
|
||||
### Docker
|
||||
@@ -413,4 +413,5 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
GitCaddy is a fork of [Gitea](https://gitea.io), the open-source self-hosted Git service. We thank the Gitea team and all contributors for building the foundation that makes GitCaddy possible.
|
||||
|
||||
- [Gitea Project](https://gitea.io)
|
||||
- [Claude Code](https://claude.ai/code) - AI-assisted development by Anthropic
|
||||
- [Gitea Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// gitea-cli is a command-line tool for interacting with Gitea instances.
|
||||
|
||||
@@ -86,9 +86,9 @@ type AppVersion struct {
|
||||
|
||||
func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
app := &cli.Command{}
|
||||
app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
app.Usage = "A painless self-hosted Git service"
|
||||
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
|
||||
app.Name = "gitcaddy-server" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
|
||||
app.Usage = "GitCaddy Server - A painless self-hosted Git service"
|
||||
app.Description = `GitCaddy Server contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes. Based on Gitea - https://gitea.io`
|
||||
app.Version = appVer.Version + appVer.Extra
|
||||
app.EnableShellCompletion = true
|
||||
app.Flags = []cli.Flag{
|
||||
|
||||
154
cmd/mcp-server/main.go
Normal file
154
cmd/mcp-server/main.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Gitea MCP Server - Model Context Protocol server for Gitea Actions
|
||||
//
|
||||
// This standalone server implements the MCP protocol over stdio,
|
||||
// proxying requests to a Gitea instance's /api/v2/mcp endpoint.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gitea-mcp-server --url https://git.example.com --token YOUR_API_TOKEN
|
||||
//
|
||||
// Configure in Claude Code's settings.json:
|
||||
//
|
||||
// {
|
||||
// "mcpServers": {
|
||||
// "gitea": {
|
||||
// "command": "gitea-mcp-server",
|
||||
// "args": ["--url", "https://git.example.com", "--token", "YOUR_TOKEN"]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
var (
|
||||
giteaURL string
|
||||
giteaToken string
|
||||
debug bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&giteaURL, "url", "", "Gitea server URL (e.g., https://git.example.com)")
|
||||
flag.StringVar(&giteaToken, "token", "", "Gitea API token")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable debug logging to stderr")
|
||||
flag.Parse()
|
||||
|
||||
// Also check environment variables
|
||||
if giteaURL == "" {
|
||||
giteaURL = os.Getenv("GITEA_URL")
|
||||
}
|
||||
if giteaToken == "" {
|
||||
giteaToken = os.Getenv("GITEA_TOKEN")
|
||||
}
|
||||
|
||||
if giteaURL == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: --url or GITEA_URL is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
debugLog("Gitea MCP Server starting")
|
||||
debugLog("Connecting to: %s", giteaURL)
|
||||
|
||||
// Read JSON-RPC messages from stdin, forward to Gitea, write responses to stdout
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
debugLog("EOF received, exiting")
|
||||
break
|
||||
}
|
||||
debugLog("Read error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Received: %s", string(line))
|
||||
|
||||
// Forward to Gitea's MCP endpoint
|
||||
response, err := forwardToGitea(line)
|
||||
if err != nil {
|
||||
debugLog("Forward error: %v", err)
|
||||
// Send error response
|
||||
errorResp := map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": nil,
|
||||
"error": map[string]any{
|
||||
"code": -32603,
|
||||
"message": "Internal error",
|
||||
"data": err.Error(),
|
||||
},
|
||||
}
|
||||
writeResponse(errorResp)
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Response: %s", string(response))
|
||||
|
||||
// Write response to stdout
|
||||
fmt.Println(string(response))
|
||||
}
|
||||
}
|
||||
|
||||
func forwardToGitea(request []byte) ([]byte, error) {
|
||||
mcpURL := giteaURL + "/api/v2/mcp"
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, mcpURL, bytes.NewReader(request))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if giteaToken != "" {
|
||||
req.Header.Set("Authorization", "token "+giteaToken)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func writeResponse(resp any) {
|
||||
data, _ := json.Marshal(resp)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
func debugLog(format string, args ...any) {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
4
go.mod
4
go.mod
@@ -25,6 +25,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
||||
@@ -145,7 +146,6 @@ require (
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/DataDog/zstd v1.5.7 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
@@ -314,7 +314,7 @@ replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-a
|
||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||
|
||||
// Use GitCaddy fork with capability support
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.3
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.7
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
|
||||
6
go.sum
6
go.sum
@@ -29,8 +29,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.3 h1:tf625YKv1Bykxr9CIcoqilC2MWiO/yBN3srlJYnFQqM=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.3/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.7/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||
@@ -78,8 +78,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2021 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
@@ -64,6 +64,8 @@ type ActionRunner struct {
|
||||
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
||||
// CapabilitiesJSON stores structured capability information for AI consumption
|
||||
CapabilitiesJSON string `xorm:"TEXT"`
|
||||
// BandwidthTestRequestedAt tracks when a bandwidth test was requested by admin
|
||||
BandwidthTestRequestedAt timeutil.TimeStamp `xorm:"index"`
|
||||
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package migrations
|
||||
@@ -405,6 +405,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
|
||||
newMigration(329, "Add release archive columns", v1_26.AddReleaseArchiveColumns),
|
||||
newMigration(330, "Add runner capabilities column", v1_26.AddRunnerCapabilitiesColumn),
|
||||
newMigration(331, "Add is_homepage_pinned to user table", v1_26.AddIsHomepagePinnedToUser),
|
||||
newMigration(332, "Add display_title and license_type to repository", v1_26.AddDisplayTitleAndLicenseTypeToRepository),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
16
models/migrations/v1_26/v331.go
Normal file
16
models/migrations/v1_26/v331.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddIsHomepagePinnedToUser adds is_homepage_pinned column to user table for organizations
|
||||
func AddIsHomepagePinnedToUser(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
18
models/migrations/v1_26/v332.go
Normal file
18
models/migrations/v1_26/v332.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddDisplayTitleAndLicenseTypeToRepository adds display_title and license_type columns to the repository table
|
||||
func AddDisplayTitleAndLicenseTypeToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
DisplayTitle string `xorm:"VARCHAR(255)"`
|
||||
LicenseType string `xorm:"VARCHAR(50)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Repository))
|
||||
}
|
||||
@@ -596,3 +596,21 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
||||
"team_user.uid": userID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetHomepagePinnedOrganizations returns all organizations that are pinned to the homepage
|
||||
func GetHomepagePinnedOrganizations(ctx context.Context) ([]*Organization, error) {
|
||||
orgs := make([]*Organization, 0, 10)
|
||||
return orgs, db.GetEngine(ctx).
|
||||
Where("type = ?", user_model.UserTypeOrganization).
|
||||
And("is_homepage_pinned = ?", true).
|
||||
And("visibility = ?", structs.VisibleTypePublic).
|
||||
OrderBy("name ASC").
|
||||
Find(&orgs)
|
||||
}
|
||||
|
||||
// SetHomepagePinned updates the homepage pinned status for an organization
|
||||
func (org *Organization) SetHomepagePinned(ctx context.Context, pinned bool) error {
|
||||
org.IsHomepagePinned = pinned
|
||||
_, err := db.GetEngine(ctx).ID(org.ID).Cols("is_homepage_pinned").Update(org)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
@@ -144,10 +144,10 @@ func GetMemberPublicVisibility(ctx context.Context, orgID, userID int64) (bool,
|
||||
|
||||
// OrgOverviewStats represents statistics for the organization overview
|
||||
type OrgOverviewStats struct {
|
||||
MemberCount int64
|
||||
RepoCount int64
|
||||
PublicRepoCount int64
|
||||
TeamCount int64
|
||||
TotalRepos int64
|
||||
TotalMembers int64
|
||||
TotalTeams int64
|
||||
TotalStars int64
|
||||
}
|
||||
|
||||
// GetOrgMemberAndTeamCounts returns member and team counts for an organization
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
@@ -142,6 +142,12 @@ func UpdatePagesDomain(ctx context.Context, domain *PagesDomain) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ActivatePagesDomainSSL sets SSL status to active for a domain
|
||||
func ActivatePagesDomainSSL(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Cols("ssl_status").Update(&PagesDomain{SSLStatus: SSLStatusActive})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePagesDomain deletes a pages domain
|
||||
func DeletePagesDomain(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2021 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
@@ -159,6 +159,8 @@ type Repository struct {
|
||||
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
Name string `xorm:"INDEX NOT NULL"`
|
||||
Description string `xorm:"TEXT"`
|
||||
DisplayTitle string `xorm:"VARCHAR(255)"`
|
||||
LicenseType string `xorm:"VARCHAR(50)"`
|
||||
Website string `xorm:"VARCHAR(2048)"`
|
||||
OriginalServiceType api.GitServiceType `xorm:"index"`
|
||||
OriginalURL string `xorm:"VARCHAR(2048)"`
|
||||
@@ -968,6 +970,17 @@ func CountNullArchivedRepository(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
|
||||
}
|
||||
|
||||
// CountOrgRepoStars returns the total number of stars across all repos owned by an organization
|
||||
func CountOrgRepoStars(ctx context.Context, orgID int64) (int64, error) {
|
||||
var total int64
|
||||
_, err := db.GetEngine(ctx).
|
||||
Table("repository").
|
||||
Where("owner_id = ?", orgID).
|
||||
Select("COALESCE(SUM(num_stars), 0)").
|
||||
Get(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
// FixNullArchivedRepository sets is_archived to false where it is null
|
||||
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
@@ -121,6 +121,8 @@ type User struct {
|
||||
// true: the user is only allowed to see organizations/repositories that they has explicit rights to.
|
||||
// (ex: in private Gitea instances user won't be allowed to see even organizations/repositories that are set as public)
|
||||
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
// IsHomepagePinned indicates if this organization should appear on the homepage
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
AllowGitHook bool
|
||||
AllowImportLocal bool // Allow migrate repository by local path
|
||||
@@ -147,9 +149,10 @@ type User struct {
|
||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
// Preferences
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ShowHeatmapOnProfile bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
// Meta defines the meta information of a user, to be stored in the K/V table
|
||||
|
||||
137
models/user/user_pinned.go
Normal file
137
models/user/user_pinned.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// PinnedRepo represents a pinned repository for a user's profile
|
||||
type PinnedRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
DisplayOrder int `xorm:"DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
|
||||
Repo any `xorm:"-"` // Will be loaded by caller to avoid import cycle
|
||||
}
|
||||
|
||||
// TableName returns the table name for PinnedRepo
|
||||
func (p *PinnedRepo) TableName() string {
|
||||
return "user_pinned_repo"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(PinnedRepo))
|
||||
}
|
||||
|
||||
// MaxPinnedRepos is the maximum number of repos a user can pin
|
||||
const MaxPinnedRepos = 6
|
||||
|
||||
// GetPinnedRepos returns all pinned repos for a user
|
||||
func GetPinnedRepos(ctx context.Context, userID int64) ([]*PinnedRepo, error) {
|
||||
pinnedRepos := make([]*PinnedRepo, 0, MaxPinnedRepos)
|
||||
err := db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
OrderBy("display_order ASC, id ASC").
|
||||
Find(&pinnedRepos)
|
||||
return pinnedRepos, err
|
||||
}
|
||||
|
||||
// CountPinnedRepos returns the count of pinned repos for a user
|
||||
func CountPinnedRepos(ctx context.Context, userID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(new(PinnedRepo))
|
||||
}
|
||||
|
||||
// IsRepoPinnedByUser checks if a repo is pinned by a user
|
||||
func IsRepoPinnedByUser(ctx context.Context, userID, repoID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Exist(new(PinnedRepo))
|
||||
}
|
||||
|
||||
// PinRepoToUser pins a repo to a user's profile
|
||||
func PinRepoToUser(ctx context.Context, userID, repoID int64) error {
|
||||
// Check if already pinned
|
||||
exists, err := IsRepoPinnedByUser(ctx, userID, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil // Already pinned
|
||||
}
|
||||
|
||||
// Check max limit
|
||||
count, err := CountPinnedRepos(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= MaxPinnedRepos {
|
||||
return ErrPinnedRepoLimit{UserID: userID, Limit: MaxPinnedRepos}
|
||||
}
|
||||
|
||||
// Get next display order
|
||||
var maxOrder int
|
||||
_, err = db.GetEngine(ctx).
|
||||
Table("user_pinned_repo").
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(MAX(display_order), 0)").
|
||||
Get(&maxOrder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pinnedRepo := &PinnedRepo{
|
||||
UserID: userID,
|
||||
RepoID: repoID,
|
||||
DisplayOrder: maxOrder + 1,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(pinnedRepo)
|
||||
return err
|
||||
}
|
||||
|
||||
// UnpinRepoFromUser unpins a repo from a user's profile
|
||||
func UnpinRepoFromUser(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Delete(new(PinnedRepo))
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePinnedRepoOrder updates the display order of pinned repos
|
||||
func UpdatePinnedRepoOrder(ctx context.Context, userID int64, repoIDs []int64) error {
|
||||
for i, repoID := range repoIDs {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("user_id = ? AND repo_id = ?", userID, repoID).
|
||||
Cols("display_order").
|
||||
Update(&PinnedRepo{DisplayOrder: i})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePinnedReposByRepoID deletes all pins for a repo (when repo is deleted)
|
||||
func DeletePinnedReposByRepoID(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(PinnedRepo))
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrPinnedRepoLimit represents an error when user has reached pin limit
|
||||
type ErrPinnedRepoLimit struct {
|
||||
UserID int64
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (err ErrPinnedRepoLimit) Error() string {
|
||||
return "user has reached the maximum number of pinned repositories"
|
||||
}
|
||||
|
||||
// IsErrPinnedRepoLimit checks if error is ErrPinnedRepoLimit
|
||||
func IsErrPinnedRepoLimit(err error) bool {
|
||||
_, ok := err.(ErrPinnedRepoLimit)
|
||||
return ok
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package circuitbreaker implements the circuit breaker pattern for external service calls.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package errors
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package errors
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package health provides comprehensive health checking for Gitea services.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package idempotency provides middleware for idempotent POST request handling.
|
||||
|
||||
@@ -109,3 +109,7 @@ func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RawMessage is a raw encoded JSON value.
|
||||
// It implements Marshaler and Unmarshaler and can be used to delay JSON decoding.
|
||||
type RawMessage = json.RawMessage
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package operations provides tracking for long-running operations
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nuget
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pages
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
@@ -53,9 +53,22 @@ type RepositoryStruct struct {
|
||||
GitGuideRemoteName *config.Value[string]
|
||||
}
|
||||
|
||||
type ThemeStruct struct {
|
||||
DisableRegistration *config.Value[bool]
|
||||
CustomSiteIconURL *config.Value[string]
|
||||
CustomHomeLogoURL *config.Value[string]
|
||||
CustomHomeHTML *config.Value[string]
|
||||
APIHeaderURL *config.Value[string]
|
||||
CustomHomeTitle *config.Value[string]
|
||||
CustomHomeTagline *config.Value[string]
|
||||
PinnedOrgDisplayFormat *config.Value[string]
|
||||
ExploreOrgDisplayFormat *config.Value[string]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
Theme *ThemeStruct
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -74,6 +87,17 @@ func initDefaultConfig() {
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
|
||||
},
|
||||
Theme: &ThemeStruct{
|
||||
DisableRegistration: config.ValueJSON[bool]("theme.disable_registration").WithFileConfig(config.CfgSecKey{Sec: "service", Key: "DISABLE_REGISTRATION"}),
|
||||
CustomSiteIconURL: config.ValueJSON[string]("theme.custom_site_icon_url").WithDefault(""),
|
||||
CustomHomeLogoURL: config.ValueJSON[string]("theme.custom_home_logo_url").WithDefault(""),
|
||||
CustomHomeHTML: config.ValueJSON[string]("theme.custom_home_html").WithDefault(""),
|
||||
APIHeaderURL: config.ValueJSON[string]("theme.api_header_url").WithDefault(""),
|
||||
CustomHomeTitle: config.ValueJSON[string]("theme.custom_home_title").WithDefault(""),
|
||||
CustomHomeTagline: config.ValueJSON[string]("theme.custom_home_tagline").WithDefault(""),
|
||||
PinnedOrgDisplayFormat: config.ValueJSON[string]("theme.pinned_org_display_format").WithDefault("condensed"),
|
||||
ExploreOrgDisplayFormat: config.ValueJSON[string]("theme.explore_org_display_format").WithDefault("list"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvConfigKeyPrefixGitCaddy is the primary prefix for GitCaddy environment variables
|
||||
EnvConfigKeyPrefixGitCaddy = "GITCADDY__"
|
||||
// EnvConfigKeyPrefixGitea is the legacy prefix for backward compatibility
|
||||
EnvConfigKeyPrefixGitea = "GITEA__"
|
||||
EnvConfigKeySuffixFile = "__FILE"
|
||||
)
|
||||
@@ -24,7 +27,8 @@ var escapeRegex = regexp.MustCompile(escapeRegexpString)
|
||||
|
||||
func CollectEnvConfigKeys() (keys []string) {
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
|
||||
// Support both GITCADDY__ and GITEA__ prefixes
|
||||
if strings.HasPrefix(env, EnvConfigKeyPrefixGitCaddy) || strings.HasPrefix(env, EnvConfigKeyPrefixGitea) {
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
keys = append(keys, k)
|
||||
}
|
||||
@@ -41,7 +45,7 @@ func ClearEnvConfigKeys() {
|
||||
// decodeEnvSectionKey will decode a portable string encoded Section__Key pair
|
||||
// Portable strings are considered to be of the form [A-Z0-9_]*
|
||||
// We will encode a disallowed value as the UTF8 byte string preceded by _0X and
|
||||
// followed by _. E.g. _0X2C_ for a '-' and _0X2E_ for '.'
|
||||
// followed by _. E.g. _0X2C_ for a '-', and _0X2E_ for '.'.
|
||||
// Section and Key are separated by a plain '__'.
|
||||
// The entire section can be encoded as a UTF8 byte string
|
||||
func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
@@ -96,16 +100,22 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
||||
}
|
||||
|
||||
// decodeEnvironmentKey decode the environment key to section and key
|
||||
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
if !strings.HasPrefix(envKey, prefixGitea) {
|
||||
// The environment key is in the form of GITCADDY__SECTION__KEY or GITEA__SECTION__KEY (legacy)
|
||||
// or GITCADDY__SECTION__KEY__FILE / GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
var prefix string
|
||||
if strings.HasPrefix(envKey, prefixGitCaddy) {
|
||||
prefix = prefixGitCaddy
|
||||
} else if strings.HasPrefix(envKey, prefixGitea) {
|
||||
prefix = prefixGitea
|
||||
} else {
|
||||
return false, "", "", false
|
||||
}
|
||||
if strings.HasSuffix(envKey, suffixFile) {
|
||||
useFileValue = true
|
||||
envKey = envKey[:len(envKey)-len(suffixFile)]
|
||||
}
|
||||
ok, section, key = decodeEnvSectionKey(envKey[len(prefixGitea):])
|
||||
ok, section, key = decodeEnvSectionKey(envKey[len(prefix):])
|
||||
return ok, section, key, useFileValue
|
||||
}
|
||||
|
||||
@@ -119,7 +129,7 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
// parse the environment variable to config section name and key name
|
||||
envKey := before
|
||||
envValue := after
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitCaddy, EnvConfigKeyPrefixGitea, EnvConfigKeySuffixFile, envKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -167,20 +177,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
|
||||
return changed
|
||||
}
|
||||
|
||||
// InitGiteaEnvVars initializes the environment variables for gitea
|
||||
func InitGiteaEnvVars() {
|
||||
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// InitGitCaddyEnvVars initializes the environment variables for GitCaddy Server
|
||||
func InitGitCaddyEnvVars() {
|
||||
// Ideally GitCaddy should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
|
||||
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users,
|
||||
// so at the moment we could still keep "unsetting the unnecessary environments"
|
||||
|
||||
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
|
||||
// HOME is managed by GitCaddy, GitCaddy's git should use "HOME/.gitconfig".
|
||||
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
|
||||
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
|
||||
// InitGiteaEnvVars is an alias for InitGitCaddyEnvVars for backward compatibility
|
||||
func InitGiteaEnvVars() {
|
||||
InitGitCaddyEnvVars()
|
||||
}
|
||||
|
||||
func InitGiteaEnvVarsForTesting() {
|
||||
InitGiteaEnvVars()
|
||||
InitGitCaddyEnvVars()
|
||||
_ = os.Unsetenv("GIT_AUTHOR_NAME")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_EMAIL")
|
||||
_ = os.Unsetenv("GIT_AUTHOR_DATE")
|
||||
|
||||
@@ -33,28 +33,29 @@ func TestDecodeEnvSectionKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
prefix := "GITEA__"
|
||||
prefixGitCaddy := "GITCADDY__"
|
||||
prefixGitea := "GITEA__"
|
||||
suffix := "__FILE"
|
||||
|
||||
ok, section, key, file := decodeEnvironmentKey(prefix, suffix, "SEC__KEY")
|
||||
ok, section, key, file := decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "SEC__KEY")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA____KEY")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA____KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.False(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
@@ -62,19 +63,19 @@ func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
|
||||
// with "__FILE" suffix, it doesn't support to write "[sec].FILE" to config (no such key FILE is used in Gitea)
|
||||
// but it could be fixed in the future by adding a new suffix like "__VALUE" (no such key VALUE is used in Gitea either)
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__FILE")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__FILE")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, section)
|
||||
assert.Empty(t, key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, section, key, file = decodeEnvironmentKey(prefix, suffix, "GITEA__SEC__KEY__FILE")
|
||||
ok, section, key, file = decodeEnvironmentKey(prefixGitCaddy, prefixGitea, suffix, "GITEA__SEC__KEY__FILE")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "sec", section)
|
||||
assert.Equal(t, "KEY", key)
|
||||
assert.True(t, file)
|
||||
|
||||
ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "PREFIX__SEC__KEY")
|
||||
ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "", "PREFIX__SEC__KEY")
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ var (
|
||||
// AppPath represents the path to the gitea binary
|
||||
AppPath string
|
||||
|
||||
// AppWorkPath is the "working directory" of Gitea. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITEA_WORK_DIR.
|
||||
// AppWorkPath is the "working directory" of GitCaddy. It maps to the: WORK_PATH in app.ini, "--work-path" flag, environment variable GITCADDY_WORK_DIR (or GITEA_WORK_DIR for backward compatibility).
|
||||
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
|
||||
// It is used as the base path for several other paths.
|
||||
AppWorkPath string
|
||||
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
|
||||
CustomPath string // Custom directory path. Env: GITCADDY_CUSTOM (or GITEA_CUSTOM for backward compatibility)
|
||||
CustomConf string
|
||||
|
||||
appWorkPathBuiltin string
|
||||
|
||||
@@ -183,7 +183,7 @@ func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string {
|
||||
|
||||
func loadServerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("server")
|
||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("GitCaddy")
|
||||
|
||||
Domain = sec.Key("DOMAIN").MustString("localhost")
|
||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||
|
||||
@@ -96,7 +96,7 @@ func InitCfgProvider(file string) {
|
||||
|
||||
func MustInstalled() {
|
||||
if !InstallLock {
|
||||
log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`)
|
||||
log.Fatal(`Unable to load config file for a installed GitCaddy instance, you should either use "--config" to set your config file (app.ini), or run "gitcaddy-server web" command to install GitCaddy.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +176,9 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
if os.Getuid() == 0 {
|
||||
if !unsafeAllowRunAsRoot {
|
||||
// Special thanks to VLC which inspired the wording of this messaging.
|
||||
log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
log.Fatal("GitCaddy Server is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission")
|
||||
}
|
||||
log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
log.Critical("You are running GitCaddy Server using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package storage
|
||||
@@ -14,12 +14,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAzureBlobStorageIterator(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("azureBlobStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
testStorageIterator(t, setting.AzureBlobStorageType, &setting.Storage{
|
||||
// azureBlobTestConfig returns the Azure Blob storage config for tests.
|
||||
// Returns nil if Azurite is not available (skip the test).
|
||||
func azureBlobTestConfig() *setting.Storage {
|
||||
return &setting.Storage{
|
||||
AzureBlobConfig: setting.AzureBlobStorageConfig{
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
|
||||
Endpoint: "http://devstoreaccount1.azurite.local:10000",
|
||||
@@ -28,7 +26,36 @@ func TestAzureBlobStorageIterator(t *testing.T) {
|
||||
AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
|
||||
Container: "test",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// skipIfNoAzurite skips the test if Azurite service is not available.
|
||||
// In CI, we always skip since Azurite is not available in our runner environment.
|
||||
func skipIfNoAzurite(t *testing.T) {
|
||||
t.Helper()
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("azureBlobStorage requires Azurite service which is not available in CI")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureBlobStorageIterator(t *testing.T) {
|
||||
skipIfNoAzurite(t)
|
||||
|
||||
cfg := azureBlobTestConfig()
|
||||
// Try to create storage to verify Azurite is available
|
||||
s, err := NewStorage(setting.AzureBlobStorageType, cfg)
|
||||
if err != nil {
|
||||
t.Skipf("azureBlobStorage not available: %v", err)
|
||||
}
|
||||
// Clean up the test storage
|
||||
_ = s.Delete("a/1.txt")
|
||||
_ = s.Delete("ab/1.txt")
|
||||
_ = s.Delete("b/1.txt")
|
||||
_ = s.Delete("b/2.txt")
|
||||
_ = s.Delete("b/3.txt")
|
||||
_ = s.Delete("b/x 4.txt")
|
||||
|
||||
testStorageIterator(t, setting.AzureBlobStorageType, cfg)
|
||||
}
|
||||
|
||||
func TestAzureBlobStoragePath(t *testing.T) {
|
||||
@@ -58,22 +85,12 @@ func TestAzureBlobStoragePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_azureBlobObject(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("azureBlobStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
skipIfNoAzurite(t)
|
||||
|
||||
s, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{
|
||||
AzureBlobConfig: setting.AzureBlobStorageConfig{
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
|
||||
Endpoint: "http://devstoreaccount1.azurite.local:10000",
|
||||
// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key
|
||||
AccountName: "devstoreaccount1",
|
||||
AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
|
||||
Container: "test",
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
s, err := NewStorage(setting.AzureBlobStorageType, azureBlobTestConfig())
|
||||
if err != nil {
|
||||
t.Skipf("azureBlobStorage not available: %v", err)
|
||||
}
|
||||
|
||||
data := "Q2xTckt6Y1hDOWh0"
|
||||
_, err = s.Save("test.txt", strings.NewReader(data), int64(len(data)))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package storage
|
||||
@@ -16,12 +16,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMinioStorageIterator(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("minioStorage not present outside of CI")
|
||||
return
|
||||
}
|
||||
testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
|
||||
// minioTestConfig returns the Minio storage config for tests.
|
||||
func minioTestConfig() *setting.Storage {
|
||||
return &setting.Storage{
|
||||
MinioConfig: setting.MinioStorageConfig{
|
||||
Endpoint: "minio:9000",
|
||||
AccessKeyID: "123456",
|
||||
@@ -29,7 +26,36 @@ func TestMinioStorageIterator(t *testing.T) {
|
||||
Bucket: "gitea",
|
||||
Location: "us-east-1",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// skipIfNoMinio skips the test if Minio service is not available.
|
||||
// In CI, we always skip since Minio is not available in our runner environment.
|
||||
func skipIfNoMinio(t *testing.T) {
|
||||
t.Helper()
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("minioStorage requires Minio service which is not available in CI")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinioStorageIterator(t *testing.T) {
|
||||
skipIfNoMinio(t)
|
||||
|
||||
cfg := minioTestConfig()
|
||||
// Try to create storage to verify Minio is available
|
||||
s, err := NewStorage(setting.MinioStorageType, cfg)
|
||||
if err != nil {
|
||||
t.Skipf("minioStorage not available: %v", err)
|
||||
}
|
||||
// Clean up any existing test files
|
||||
_ = s.Delete("a/1.txt")
|
||||
_ = s.Delete("ab/1.txt")
|
||||
_ = s.Delete("b/1.txt")
|
||||
_ = s.Delete("b/2.txt")
|
||||
_ = s.Delete("b/3.txt")
|
||||
_ = s.Delete("b/x 4.txt")
|
||||
|
||||
testStorageIterator(t, setting.MinioStorageType, cfg)
|
||||
}
|
||||
|
||||
func TestMinioStoragePath(t *testing.T) {
|
||||
@@ -67,10 +93,8 @@ func TestMinioStoragePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestS3StorageBadRequest(t *testing.T) {
|
||||
if os.Getenv("CI") == "" {
|
||||
t.Skip("S3Storage not present outside of CI")
|
||||
return
|
||||
}
|
||||
skipIfNoMinio(t)
|
||||
|
||||
cfg := &setting.Storage{
|
||||
MinioConfig: setting.MinioStorageConfig{
|
||||
Endpoint: "minio:9000",
|
||||
|
||||
@@ -1,19 +1,59 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// BandwidthInfo holds network bandwidth test results
|
||||
type BandwidthInfo struct {
|
||||
DownloadMbps float64 `json:"download_mbps"`
|
||||
UploadMbps float64 `json:"upload_mbps,omitempty"`
|
||||
Latency float64 `json:"latency_ms,omitempty"`
|
||||
TestedAt time.Time `json:"tested_at"`
|
||||
}
|
||||
|
||||
// DiskInfo holds disk space information for a runner
|
||||
type DiskInfo struct {
|
||||
Total uint64 `json:"total_bytes"`
|
||||
Free uint64 `json:"free_bytes"`
|
||||
Used uint64 `json:"used_bytes"`
|
||||
UsedPercent float64 `json:"used_percent"`
|
||||
}
|
||||
|
||||
// DistroInfo holds Linux distribution information
|
||||
type DistroInfo struct {
|
||||
ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora"
|
||||
VersionID string `json:"version_id,omitempty"` // e.g., "24.04", "12"
|
||||
PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS"
|
||||
}
|
||||
|
||||
// XcodeInfo holds Xcode-specific information for macOS runners
|
||||
type XcodeInfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
SDKs []string `json:"sdks,omitempty"`
|
||||
Simulators []string `json:"simulators,omitempty"`
|
||||
}
|
||||
|
||||
// RunnerCapability represents the detailed capabilities of a runner
|
||||
type RunnerCapability struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Distro *DistroInfo `json:"distro,omitempty"`
|
||||
Xcode *XcodeInfo `json:"xcode,omitempty"`
|
||||
Docker bool `json:"docker"`
|
||||
DockerCompose bool `json:"docker_compose"`
|
||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||
Shell []string `json:"shell,omitempty"`
|
||||
Tools map[string][]string `json:"tools,omitempty"`
|
||||
BuildTools []string `json:"build_tools,omitempty"`
|
||||
PackageManagers []string `json:"package_managers,omitempty"`
|
||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Disk *DiskInfo `json:"disk,omitempty"`
|
||||
Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"`
|
||||
SuggestedLabels []string `json:"suggested_labels,omitempty"`
|
||||
}
|
||||
|
||||
// CapabilityFeatures represents feature support flags
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs // import "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
@@ -179,10 +179,10 @@ type OrgOverview struct {
|
||||
|
||||
// OrgOverviewStats represents organization statistics
|
||||
type OrgOverviewStats struct {
|
||||
MemberCount int64 `json:"member_count"`
|
||||
RepoCount int64 `json:"repo_count"`
|
||||
PublicRepoCount int64 `json:"public_repo_count"`
|
||||
TeamCount int64 `json:"team_count"`
|
||||
TotalRepos int64 `json:"total_repos"`
|
||||
TotalMembers int64 `json:"total_members"`
|
||||
TotalTeams int64 `json:"total_teams"`
|
||||
TotalStars int64 `json:"total_stars"`
|
||||
}
|
||||
|
||||
// OrgProfileContent represents the organization profile content
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2016 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
@@ -38,6 +38,8 @@ type AddPagesDomainOption struct {
|
||||
// The custom domain to add
|
||||
// required: true
|
||||
Domain string `json:"domain" binding:"Required"`
|
||||
// Mark SSL as handled externally (e.g., by Cloudflare)
|
||||
SSLExternal bool `json:"ssl_external"`
|
||||
}
|
||||
|
||||
// PagesInfo represents the full pages information for a repository
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewFuncMap() template.FuncMap {
|
||||
"QueryEscape": queryEscape,
|
||||
"QueryBuild": QueryBuild,
|
||||
"SanitizeHTML": SanitizeHTML,
|
||||
"SafeHTML": SafeHTML,
|
||||
"URLJoin": util.URLJoin,
|
||||
"DotEscape": dotEscape,
|
||||
|
||||
@@ -46,10 +47,19 @@ func NewFuncMap() template.FuncMap {
|
||||
"PathEscapeSegments": util.PathEscapeSegments,
|
||||
|
||||
// utils
|
||||
"StringUtils": NewStringUtils,
|
||||
"SliceUtils": NewSliceUtils,
|
||||
"JsonUtils": NewJsonUtils,
|
||||
"DateUtils": NewDateUtils,
|
||||
"StringUtils": NewStringUtils,
|
||||
"SliceUtils": NewSliceUtils,
|
||||
"newSlice": func() []any { return []any{} },
|
||||
"Append": func(s []any, v any) []any { return append(s, v) },
|
||||
"Int64ToFloat64": func(i uint64) float64 { return float64(i) },
|
||||
"DivideFloat64": func(a, b float64) float64 {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
},
|
||||
"JsonUtils": NewJsonUtils,
|
||||
"DateUtils": NewDateUtils,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// svg / avatar / icon / color
|
||||
@@ -163,6 +173,11 @@ func SanitizeHTML(s string) template.HTML {
|
||||
return markup.Sanitize(s)
|
||||
}
|
||||
|
||||
// SafeHTML marks a string as safe HTML (no sanitization). Use with caution - only for trusted admin content.
|
||||
func SafeHTML(s string) template.HTML {
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
func htmlFormat(s any, args ...any) template.HTML {
|
||||
if len(args) == 0 {
|
||||
// to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
@@ -33,3 +33,17 @@ func (su *SliceUtils) Contains(s, v any) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Append appends an element to a slice and returns the new slice
|
||||
func (su *SliceUtils) Append(s, v any) any {
|
||||
if s == nil {
|
||||
return []any{v}
|
||||
}
|
||||
sv := reflect.ValueOf(s)
|
||||
if sv.Kind() != reflect.Slice {
|
||||
panic(fmt.Sprintf("invalid type, expected slice, but got: %T", s))
|
||||
}
|
||||
// Create a new slice with the appended element
|
||||
newSlice := reflect.Append(sv, reflect.ValueOf(v))
|
||||
return newSlice.Interface()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
@@ -35,6 +35,7 @@ func (su *StringUtils) ToString(v any) string {
|
||||
func (su *StringUtils) HasPrefix(s, prefix string) bool {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
func (su *StringUtils) HasSuffix(s, suffix string) bool { return strings.HasSuffix(s, suffix) }
|
||||
|
||||
func (su *StringUtils) Contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
@@ -61,6 +62,10 @@ func (su *StringUtils) ToUpper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
func (su *StringUtils) ToLower(s string) string {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
func (su *StringUtils) TrimPrefix(s, prefix string) string {
|
||||
return strings.TrimPrefix(s, prefix)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package validation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package middleware
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package middleware
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
19
public/assets/img/gitcaddy-icon.svg
Normal file
19
public/assets/img/gitcaddy-icon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="374px" height="374px" viewBox="397.5 371.5 374 374" style="enable-background:new 397.5 371.5 374 374;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#609926;" d="M526.05,605.17c-0.69,0-1.39-0.14-2.08-0.46l-82.52-37.42v95.65l138.07,48.38V563.78l-50.37,40.29
|
||||
C528.27,604.76,527.16,605.17,526.05,605.17L526.05,605.17z"/>
|
||||
<path style="fill:#609926;" d="M643.03,605.17c-1.16,0-2.26-0.42-3.15-1.11l-50.37-40.29v147.55l138.16-48.38v-95.65l-82.56,37.42
|
||||
c-0.65,0.28-1.34,0.46-2.04,0.46L643.03,605.17z"/>
|
||||
<path style="fill:#609926;" d="M525.31,594.35l49.08-39.22l-137-48.06l-37.88,30.34L525.31,594.35z"/>
|
||||
<path style="fill:#609926;" d="M594.69,555.13l49.03,39.22l125.81-56.94l-37.88-30.34L594.69,555.13z"/>
|
||||
<path style="fill:#609926;" d="M525.36,438.98c-11.01-3.42-23.22-7.26-36.22-13.46c-1.62,64.89,35.94,80.94,52.68,84.88
|
||||
c16.47,3.88,30.57,0.55,35.48-3.88c-11.24-14.06-25.81-26.6-43.43-31.45c16.19,1.76,31.13,9.62,43.99,19.89
|
||||
c-3.88-40.66-22.76-46.62-52.45-55.97H525.36z"/>
|
||||
<path style="fill:#609926;" d="M589.74,491.11c5.23,5,20.81,8.79,38.85,4.53c18.22-4.3,59.16-21.79,57.22-92.6
|
||||
c-14.2,6.8-27.57,10.96-39.59,14.76c-32.47,10.17-53.01,16.6-56.98,61.52c13.92-11.38,30.16-20.26,47.96-22.29
|
||||
c-19.24,5.23-35.2,18.87-47.41,34.04L589.74,491.11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1062
public/assets/img/gitcaddy-logo.svg
Normal file
1062
public/assets/img/gitcaddy-logo.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 105 KiB |
27649
public/assets/licenses.txt
Normal file
27649
public/assets/licenses.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package runner
|
||||
@@ -115,10 +115,10 @@ func (s *Service) Declare(
|
||||
req *connect.Request[runnerv1.DeclareRequest],
|
||||
) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
runner := GetRunner(ctx)
|
||||
runner.AgentLabels = req.Msg.Labels
|
||||
runner.Version = req.Msg.Version
|
||||
runner.CapabilitiesJSON = req.Msg.CapabilitiesJson
|
||||
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version", "capabilities_json"); err != nil {
|
||||
if err := actions_model.UpdateRunner(ctx, runner, "version", "capabilities_json"); err != nil {
|
||||
log.Error("Declare: failed to update runner %d: %v", runner.ID, err)
|
||||
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
||||
}
|
||||
|
||||
@@ -141,6 +141,15 @@ func (s *Service) FetchTask(
|
||||
) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
||||
runner := GetRunner(ctx)
|
||||
|
||||
// Update runner capabilities if provided
|
||||
if capsJSON := req.Msg.GetCapabilitiesJson(); capsJSON != "" && capsJSON != runner.CapabilitiesJSON {
|
||||
runner.CapabilitiesJSON = capsJSON
|
||||
if err := actions_model.UpdateRunner(ctx, runner, "capabilities_json"); err != nil {
|
||||
log.Warn("failed to update runner capabilities: %v", err)
|
||||
// Don't return error, just log warning - capabilities update is not critical
|
||||
}
|
||||
}
|
||||
|
||||
var task *runnerv1.Task
|
||||
tasksVersion := req.Msg.TasksVersion // task version from runner
|
||||
latestVersion, err := actions_model.GetTasksVersionByScope(ctx, runner.OwnerID, runner.RepoID)
|
||||
@@ -167,9 +176,22 @@ func (s *Service) FetchTask(
|
||||
task = t
|
||||
}
|
||||
}
|
||||
|
||||
// Check if admin requested a bandwidth test
|
||||
requestBandwidthTest := false
|
||||
if runner.BandwidthTestRequestedAt > 0 {
|
||||
requestBandwidthTest = true
|
||||
// Clear the request after sending
|
||||
runner.BandwidthTestRequestedAt = 0
|
||||
if err := actions_model.UpdateRunner(ctx, runner, "bandwidth_test_requested_at"); err != nil {
|
||||
log.Warn("failed to clear bandwidth test request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
res := connect.NewResponse(&runnerv1.FetchTaskResponse{
|
||||
Task: task,
|
||||
TasksVersion: latestVersion,
|
||||
Task: task,
|
||||
TasksVersion: latestVersion,
|
||||
RequestBandwidthTest: requestBandwidthTest,
|
||||
})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2016 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package v1 Gitea API
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
@@ -92,10 +92,10 @@ func GetOverview(ctx *context.APIContext) {
|
||||
PublicMembers: apiPublicMembers,
|
||||
TotalMembers: totalMembers,
|
||||
Stats: &api.OrgOverviewStats{
|
||||
MemberCount: stats.MemberCount,
|
||||
RepoCount: stats.RepoCount,
|
||||
PublicRepoCount: stats.PublicRepoCount,
|
||||
TeamCount: stats.TeamCount,
|
||||
TotalRepos: stats.TotalRepos,
|
||||
TotalMembers: stats.TotalMembers,
|
||||
TotalTeams: stats.TotalTeams,
|
||||
TotalStars: stats.TotalStars,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
@@ -219,7 +219,7 @@ func AddPagesDomain(ctx *context.APIContext) {
|
||||
|
||||
form := web.GetForm(ctx).(*api.AddPagesDomainOption)
|
||||
|
||||
domain, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, form.Domain)
|
||||
domain, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, form.Domain, form.SSLExternal)
|
||||
if err != nil {
|
||||
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Domain already exists")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2016 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package v2 Gitea API v2
|
||||
@@ -86,6 +86,9 @@ func Routes() *web.Router {
|
||||
m.Get("/component/{component}", ComponentHealthCheck)
|
||||
})
|
||||
|
||||
// MCP Protocol endpoint for AI tool integration
|
||||
m.Post("/mcp", MCPHandler)
|
||||
|
||||
// Operation progress endpoints (SSE)
|
||||
m.Group("/operations", func() {
|
||||
m.Get("/{id}/progress", OperationProgress)
|
||||
@@ -140,6 +143,8 @@ func Routes() *web.Router {
|
||||
// Actions v2 API - AI-friendly runner capability discovery
|
||||
m.Group("/repos/{owner}/{repo}/actions", func() {
|
||||
m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities)
|
||||
m.Get("/runners/status", repoAssignment(), ListRunnersStatus)
|
||||
m.Get("/runners/{runner_id}/status", repoAssignment(), GetRunnerStatus)
|
||||
m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
778
routers/api/v2/mcp.go
Normal file
778
routers/api/v2/mcp.go
Normal file
@@ -0,0 +1,778 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// MCP Protocol Types (JSON-RPC 2.0)
|
||||
|
||||
type MCPRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type MCPResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID any `json:"id"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *MCPError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type MCPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// MCP Tool definitions
|
||||
type MCPTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]any `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type MCPToolsListResult struct {
|
||||
Tools []MCPTool `json:"tools"`
|
||||
}
|
||||
|
||||
type MCPToolCallParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
}
|
||||
|
||||
type MCPToolCallResult struct {
|
||||
Content []MCPContent `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
type MCPContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type MCPInitializeParams struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
ClientInfo map[string]string `json:"clientInfo"`
|
||||
}
|
||||
|
||||
type MCPInitializeResult struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
ServerInfo map[string]string `json:"serverInfo"`
|
||||
}
|
||||
|
||||
// Available MCP tools
|
||||
var mcpTools = []MCPTool{
|
||||
{
|
||||
Name: "list_runners",
|
||||
Description: "List all runners with their status, capabilities, and current workload",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner (optional, lists global runners if omitted)",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name (optional)",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"online", "offline", "all"},
|
||||
"description": "Filter by runner status",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_runner",
|
||||
Description: "Get detailed information about a specific runner including capabilities, disk space, and bandwidth",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"runner_id": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "The runner ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"runner_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_workflow_runs",
|
||||
Description: "List workflow runs for a repository with status and timing information",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"pending", "running", "success", "failure", "cancelled", "all"},
|
||||
"description": "Filter by run status",
|
||||
},
|
||||
"limit": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Maximum number of runs to return (default 20)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_workflow_run",
|
||||
Description: "Get detailed information about a specific workflow run including all jobs and their status",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"run_id": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "The workflow run ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "run_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_job_logs",
|
||||
Description: "Get logs from a specific job in a workflow run",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"job_id": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "The job ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "job_id"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_releases",
|
||||
Description: "List releases for a repository",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"limit": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Maximum number of releases to return (default 10)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_release",
|
||||
Description: "Get details of a specific release including all assets",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"tag": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Release tag (e.g., v1.0.0)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "tag"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MCPHandler handles MCP protocol requests
|
||||
// @Summary MCP Protocol Endpoint
|
||||
// @Description Handles Model Context Protocol requests for AI tool integration
|
||||
// @Tags mcp
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} MCPResponse
|
||||
// @Router /mcp [post]
|
||||
func MCPHandler(ctx *context.APIContext) {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var req MCPRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
sendMCPError(ctx, nil, -32700, "Parse error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.JSONRPC != "2.0" {
|
||||
sendMCPError(ctx, req.ID, -32600, "Invalid Request", "jsonrpc must be 2.0")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("MCP request: method=%s id=%v", req.Method, req.ID)
|
||||
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
handleInitialize(ctx, &req)
|
||||
case "tools/list":
|
||||
handleToolsList(ctx, &req)
|
||||
case "tools/call":
|
||||
handleToolsCall(ctx, &req)
|
||||
case "ping":
|
||||
sendMCPResult(ctx, req.ID, map[string]string{})
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32601, "Method not found", "Unknown method: "+req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func handleInitialize(ctx *context.APIContext, req *MCPRequest) {
|
||||
result := MCPInitializeResult{
|
||||
ProtocolVersion: "2024-11-05",
|
||||
Capabilities: map[string]any{
|
||||
"tools": map[string]any{},
|
||||
},
|
||||
ServerInfo: map[string]string{
|
||||
"name": "gitea-actions",
|
||||
"version": setting.AppVer,
|
||||
},
|
||||
}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
|
||||
func handleToolsList(ctx *context.APIContext, req *MCPRequest) {
|
||||
result := MCPToolsListResult{Tools: mcpTools}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
|
||||
func handleToolsCall(ctx *context.APIContext, req *MCPRequest) {
|
||||
var params MCPToolCallParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
sendMCPError(ctx, req.ID, -32602, "Invalid params", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var result any
|
||||
var err error
|
||||
|
||||
switch params.Name {
|
||||
case "list_runners":
|
||||
result, err = toolListRunners(ctx, params.Arguments)
|
||||
case "get_runner":
|
||||
result, err = toolGetRunner(ctx, params.Arguments)
|
||||
case "list_workflow_runs":
|
||||
result, err = toolListWorkflowRuns(ctx, params.Arguments)
|
||||
case "get_workflow_run":
|
||||
result, err = toolGetWorkflowRun(ctx, params.Arguments)
|
||||
case "get_job_logs":
|
||||
result, err = toolGetJobLogs(ctx, params.Arguments)
|
||||
case "list_releases":
|
||||
result, err = toolListReleases(ctx, params.Arguments)
|
||||
case "get_release":
|
||||
result, err = toolGetRelease(ctx, params.Arguments)
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sendMCPToolResult(ctx, req.ID, err.Error(), true)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert result to JSON text
|
||||
jsonBytes, _ := json.MarshalIndent(result, "", " ")
|
||||
sendMCPToolResult(ctx, req.ID, string(jsonBytes), false)
|
||||
}
|
||||
|
||||
func sendMCPResult(ctx *context.APIContext, id, result any) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
func sendMCPError(ctx *context.APIContext, id any, code int, message, data string) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Error: &MCPError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Data: data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sendMCPToolResult(ctx *context.APIContext, id any, text string, isError bool) {
|
||||
ctx.JSON(http.StatusOK, MCPResponse{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Result: MCPToolCallResult{
|
||||
Content: []MCPContent{{Type: "text", Text: text}},
|
||||
IsError: isError,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
|
||||
func toolListRunners(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
var runners actions_model.RunnerList
|
||||
var err error
|
||||
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner != "" && repo != "" {
|
||||
// Get repo-specific runners
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
runners, err = actions_model.GetRunnersOfRepo(ctx, repository.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Get all runners (admin)
|
||||
opts := actions_model.FindRunnerOptions{}
|
||||
opts.PageSize = 100
|
||||
runners, err = db.Find[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
statusFilter, _ := args["status"].(string)
|
||||
|
||||
result := make([]map[string]any, 0, len(runners))
|
||||
for _, runner := range runners {
|
||||
isOnline := runner.IsOnline()
|
||||
|
||||
if statusFilter == "online" && !isOnline {
|
||||
continue
|
||||
}
|
||||
if statusFilter == "offline" && isOnline {
|
||||
continue
|
||||
}
|
||||
|
||||
r := map[string]any{
|
||||
"id": runner.ID,
|
||||
"name": runner.Name,
|
||||
"is_online": isOnline,
|
||||
"status": runner.Status().String(),
|
||||
"version": runner.Version,
|
||||
"labels": runner.AgentLabels,
|
||||
"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Parse capabilities if available
|
||||
if runner.CapabilitiesJSON != "" {
|
||||
var caps api.RunnerCapability
|
||||
if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
|
||||
r["capabilities"] = caps
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"runners": result,
|
||||
"count": len(result),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetRunner(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
runnerIDFloat, ok := args["runner_id"].(float64)
|
||||
if !ok {
|
||||
return nil, errors.New("runner_id is required")
|
||||
}
|
||||
runnerID := int64(runnerIDFloat)
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("runner not found: %d", runnerID)
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"id": runner.ID,
|
||||
"name": runner.Name,
|
||||
"is_online": runner.IsOnline(),
|
||||
"status": runner.Status().String(),
|
||||
"version": runner.Version,
|
||||
"labels": runner.AgentLabels,
|
||||
"last_online": runner.LastOnline.AsTime().Format(time.RFC3339),
|
||||
"repo_id": runner.RepoID,
|
||||
"owner_id": runner.OwnerID,
|
||||
}
|
||||
|
||||
if runner.CapabilitiesJSON != "" {
|
||||
var caps api.RunnerCapability
|
||||
if json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps) == nil {
|
||||
result["capabilities"] = caps
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toolListWorkflowRuns(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner and repo are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if l, ok := args["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
|
||||
opts := actions_model.FindRunOptions{
|
||||
RepoID: repository.ID,
|
||||
}
|
||||
opts.PageSize = limit
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statusFilter, _ := args["status"].(string)
|
||||
|
||||
result := make([]map[string]any, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
status := run.Status.String()
|
||||
|
||||
if statusFilter != "" && statusFilter != "all" && !strings.EqualFold(status, statusFilter) {
|
||||
continue
|
||||
}
|
||||
|
||||
r := map[string]any{
|
||||
"id": run.ID,
|
||||
"title": run.Title,
|
||||
"status": status,
|
||||
"event": string(run.Event),
|
||||
"workflow_id": run.WorkflowID,
|
||||
"ref": run.Ref,
|
||||
"commit_sha": run.CommitSHA,
|
||||
"started": run.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": run.Stopped.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"runs": result,
|
||||
"count": len(result),
|
||||
"repo": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetWorkflowRun(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
runIDFloat, ok := args["run_id"].(float64)
|
||||
|
||||
if owner == "" || repo == "" || !ok {
|
||||
return nil, errors.New("owner, repo, and run_id are required")
|
||||
}
|
||||
runID := int64(runIDFloat)
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, runID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run not found: %d", runID)
|
||||
}
|
||||
|
||||
// Get jobs for this run
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jobResults := make([]map[string]any, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
j := map[string]any{
|
||||
"id": job.ID,
|
||||
"name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"started": job.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": job.Stopped.AsTime().Format(time.RFC3339),
|
||||
"task_id": job.TaskID,
|
||||
}
|
||||
jobResults = append(jobResults, j)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"id": run.ID,
|
||||
"title": run.Title,
|
||||
"status": run.Status.String(),
|
||||
"event": string(run.Event),
|
||||
"workflow_id": run.WorkflowID,
|
||||
"ref": run.Ref,
|
||||
"commit_sha": run.CommitSHA,
|
||||
"started": run.Started.AsTime().Format(time.RFC3339),
|
||||
"stopped": run.Stopped.AsTime().Format(time.RFC3339),
|
||||
"jobs": jobResults,
|
||||
"job_count": len(jobResults),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetJobLogs(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
jobIDFloat, ok := args["job_id"].(float64)
|
||||
|
||||
if owner == "" || repo == "" || !ok {
|
||||
return nil, errors.New("owner, repo, and job_id are required")
|
||||
}
|
||||
jobID := int64(jobIDFloat)
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
job, err := actions_model.GetRunJobByID(ctx, jobID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("job not found: %d", jobID)
|
||||
}
|
||||
|
||||
// Verify job belongs to this repo
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repository.ID, job.RunID)
|
||||
if err != nil {
|
||||
return nil, errors.New("job not found in repository")
|
||||
}
|
||||
_ = run
|
||||
|
||||
// Get the task for this job
|
||||
if job.TaskID == 0 {
|
||||
return map[string]any{
|
||||
"job_id": jobID,
|
||||
"job_name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"message": "Job has not started yet - no task assigned",
|
||||
}, nil
|
||||
}
|
||||
|
||||
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("task not found for job: %d", jobID)
|
||||
}
|
||||
|
||||
// Check if logs are expired
|
||||
if task.LogExpired {
|
||||
return map[string]any{
|
||||
"job_id": jobID,
|
||||
"job_name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"message": "Logs have expired",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get steps for this task
|
||||
steps := actions.FullSteps(task)
|
||||
|
||||
stepLogs := make([]map[string]any, 0, len(steps))
|
||||
for i, step := range steps {
|
||||
stepInfo := map[string]any{
|
||||
"step": i,
|
||||
"name": step.Name,
|
||||
"status": step.Status.String(),
|
||||
"duration": step.Duration().String(),
|
||||
}
|
||||
|
||||
// Read logs for this step
|
||||
if step.LogLength > 0 && step.LogIndex < int64(len(task.LogIndexes)) {
|
||||
offset := task.LogIndexes[step.LogIndex]
|
||||
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, step.LogLength)
|
||||
if err != nil {
|
||||
stepInfo["error"] = fmt.Sprintf("failed to read logs: %v", err)
|
||||
} else {
|
||||
lines := make([]string, 0, len(logRows))
|
||||
for _, row := range logRows {
|
||||
lines = append(lines, row.Content)
|
||||
}
|
||||
stepInfo["lines"] = lines
|
||||
stepInfo["line_count"] = len(lines)
|
||||
}
|
||||
}
|
||||
|
||||
stepLogs = append(stepLogs, stepInfo)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"job_id": jobID,
|
||||
"job_name": job.Name,
|
||||
"status": job.Status.String(),
|
||||
"task_id": task.ID,
|
||||
"log_expired": task.LogExpired,
|
||||
"steps": stepLogs,
|
||||
"step_count": len(stepLogs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolListReleases(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner and repo are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if l, ok := args["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
|
||||
opts := repo_model.FindReleasesOptions{
|
||||
RepoID: repository.ID,
|
||||
}
|
||||
opts.PageSize = limit
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]any, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
r := map[string]any{
|
||||
"id": release.ID,
|
||||
"tag_name": release.TagName,
|
||||
"title": release.Title,
|
||||
"is_draft": release.IsDraft,
|
||||
"is_prerelease": release.IsPrerelease,
|
||||
"created_at": release.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"releases": result,
|
||||
"count": len(result),
|
||||
"repo": fmt.Sprintf("%s/%s", owner, repo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetRelease(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
tag, _ := args["tag"].(string)
|
||||
|
||||
if owner == "" || repo == "" || tag == "" {
|
||||
return nil, errors.New("owner, repo, and tag are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, repository.ID, tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release not found: %s", tag)
|
||||
}
|
||||
|
||||
// Load attachments
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets := make([]map[string]any, 0, len(release.Attachments))
|
||||
for _, att := range release.Attachments {
|
||||
assets = append(assets, map[string]any{
|
||||
"id": att.ID,
|
||||
"name": att.Name,
|
||||
"size": att.Size,
|
||||
"download_count": att.DownloadCount,
|
||||
"download_url": fmt.Sprintf("%s/%s/%s/releases/download/%s/%s",
|
||||
setting.AppURL, owner, repo, tag, att.Name),
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"id": release.ID,
|
||||
"tag_name": release.TagName,
|
||||
"title": release.Title,
|
||||
"body": release.Note,
|
||||
"is_draft": release.IsDraft,
|
||||
"is_prerelease": release.IsPrerelease,
|
||||
"created_at": release.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
"assets": assets,
|
||||
"asset_count": len(assets),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
134
routers/api/v2/runners.go
Normal file
134
routers/api/v2/runners.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// RunnerStatusResponse represents the runner status for API/polling
|
||||
type RunnerStatusResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
Labels []string `json:"labels"`
|
||||
LastOnline *time.Time `json:"last_online,omitempty"`
|
||||
Capabilities *api.RunnerCapability `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// GetRunnerStatus returns the current status of a runner
|
||||
// @Summary Get runner status
|
||||
// @Description Returns current runner status including online state, capabilities, disk, and bandwidth
|
||||
// @Tags actions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param owner path string true "owner of the repo"
|
||||
// @Param repo path string true "name of the repo"
|
||||
// @Param runner_id path int64 true "runner ID"
|
||||
// @Success 200 {object} RunnerStatusResponse
|
||||
// @Router /repos/{owner}/{repo}/actions/runners/{runner_id}/status [get]
|
||||
func GetRunnerStatus(ctx *context.APIContext) {
|
||||
runnerID := ctx.PathParamInt64("runner_id")
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check access - runner must belong to this repo or be global
|
||||
repo := ctx.Repo.Repository
|
||||
if runner.RepoID != 0 && runner.RepoID != repo.ID {
|
||||
ctx.APIErrorNotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
response := buildRunnerStatusResponse(runner)
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetAdminRunnerStatus returns the current status of a runner (admin endpoint)
|
||||
// @Summary Get runner status (admin)
|
||||
// @Description Returns current runner status for admin panel AJAX polling
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param runner_id path int64 true "runner ID"
|
||||
// @Success 200 {object} RunnerStatusResponse
|
||||
// @Router /admin/actions/runners/{runner_id}/status [get]
|
||||
func GetAdminRunnerStatus(ctx *context.APIContext) {
|
||||
runnerID := ctx.PathParamInt64("runner_id")
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
response := buildRunnerStatusResponse(runner)
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// buildRunnerStatusResponse creates a status response from a runner
|
||||
func buildRunnerStatusResponse(runner *actions_model.ActionRunner) *RunnerStatusResponse {
|
||||
response := &RunnerStatusResponse{
|
||||
ID: runner.ID,
|
||||
Name: runner.Name,
|
||||
IsOnline: runner.IsOnline(),
|
||||
Status: runner.Status().String(),
|
||||
Version: runner.Version,
|
||||
Labels: runner.AgentLabels,
|
||||
}
|
||||
|
||||
// Add last online time if available
|
||||
if runner.LastOnline.AsTime().Unix() > 0 {
|
||||
lastOnline := runner.LastOnline.AsTime()
|
||||
response.LastOnline = &lastOnline
|
||||
}
|
||||
|
||||
// Parse capabilities JSON if available
|
||||
if runner.CapabilitiesJSON != "" {
|
||||
var caps api.RunnerCapability
|
||||
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps); err == nil {
|
||||
response.Capabilities = &caps
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ListRunnersStatus returns status for all runners accessible to the repo
|
||||
// @Summary List runner statuses
|
||||
// @Description Returns status for all runners available to the repository
|
||||
// @Tags actions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param owner path string true "owner of the repo"
|
||||
// @Param repo path string true "name of the repo"
|
||||
// @Success 200 {array} RunnerStatusResponse
|
||||
// @Router /repos/{owner}/{repo}/actions/runners/status [get]
|
||||
func ListRunnersStatus(ctx *context.APIContext) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
runners, err := actions_model.GetRunnersOfRepo(ctx, repo.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*RunnerStatusResponse, 0, len(runners))
|
||||
for _, runner := range runners {
|
||||
responses = append(responses, buildRunnerStatusResponse(runner))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, responses)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2016 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package routers
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -235,6 +238,13 @@ func ChangeConfig(ctx *context.Context) {
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
cfg.Theme.DisableRegistration.DynKey(): marshalBool,
|
||||
cfg.Theme.CustomHomeHTML.DynKey(): marshalString(""),
|
||||
cfg.Theme.APIHeaderURL.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTitle.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTagline.DynKey(): marshalString(""),
|
||||
cfg.Theme.PinnedOrgDisplayFormat.DynKey(): marshalString("condensed"),
|
||||
cfg.Theme.ExploreOrgDisplayFormat.DynKey(): marshalString("list"),
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
@@ -272,3 +282,167 @@ loop:
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// ChangeThemeLogo handles homepage logo upload and custom URL
|
||||
func ChangeThemeLogo(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("logo_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".jpg": true, ".jpeg": true, ".gif": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.logo_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0o755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-home-logo" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_logo_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
// ChangeThemeIcon handles site icon (favicon + navbar) upload and custom URL
|
||||
func ChangeThemeIcon(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("icon_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".ico": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.icon_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0o755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-site-icon" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_icon_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@@ -40,7 +39,7 @@ func Organizations(ctx *context.Context) {
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder = "alphabetically"
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -61,6 +62,14 @@ func Home(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PageIsHome"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
// Load pinned organizations for homepage
|
||||
pinnedOrgs, err := organization_model.GetHomepagePinnedOrganizations(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetHomepagePinnedOrganizations: %v", err)
|
||||
} else {
|
||||
ctx.Data["PinnedOrganizations"] = pinnedOrgs
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplHome)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
@@ -17,12 +17,21 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// RecentRepoActivity holds repo and its latest commit info
|
||||
type RecentRepoActivity struct {
|
||||
Repo *repo_model.Repository
|
||||
CommitMessage string
|
||||
CommitTime timeutil.TimeStamp
|
||||
}
|
||||
|
||||
const tplOrgHome templates.TplName = "org/home"
|
||||
|
||||
// Home show organization home page
|
||||
@@ -103,6 +112,43 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
|
||||
|
||||
// Load recently updated repositories for activity section
|
||||
// Only show private repos if user is signed in and is org member
|
||||
showPrivate := ctx.IsSigned && ctx.Org.IsMember
|
||||
recentRepos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: 10,
|
||||
Page: 1,
|
||||
},
|
||||
OwnerID: org.ID,
|
||||
OrderBy: db.SearchOrderByRecentUpdated,
|
||||
Private: showPrivate,
|
||||
Actor: ctx.Doer,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("SearchRepository for recent repos: %v", err)
|
||||
} else {
|
||||
// Load commit info for each repo
|
||||
var recentActivity []*RecentRepoActivity
|
||||
for _, repo := range recentRepos {
|
||||
activity := &RecentRepoActivity{Repo: repo}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
if err == nil {
|
||||
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||
if err == nil {
|
||||
activity.CommitMessage = commit.Summary()
|
||||
activity.CommitTime = timeutil.TimeStamp(commit.Author.When.Unix())
|
||||
}
|
||||
|
||||
gitRepo.Close()
|
||||
}
|
||||
recentActivity = append(recentActivity, activity)
|
||||
}
|
||||
ctx.Data["RecentActivity"] = recentActivity
|
||||
}
|
||||
|
||||
prepareResult, err := shared_user.RenderUserOrgHeader(ctx)
|
||||
if err != nil {
|
||||
@@ -157,12 +203,10 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
}
|
||||
ctx.Data["OrgStats"] = orgStats
|
||||
|
||||
// if no profile readme, it still means "view repositories"
|
||||
isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult)
|
||||
// Also show overview if there are pinned repos even without profile readme
|
||||
if !viewRepositories && len(pinnedRepos) > 0 {
|
||||
isViewOverview = true
|
||||
}
|
||||
// Always show overview by default for organizations
|
||||
isViewOverview := !viewRepositories
|
||||
// Load profile readme if available
|
||||
prepareOrgProfileReadme(ctx, prepareResult)
|
||||
ctx.Data["PageIsViewRepositories"] = !isViewOverview
|
||||
ctx.Data["PageIsViewOverview"] = isViewOverview
|
||||
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
|
||||
@@ -242,3 +286,45 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
|
||||
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateProfileRepo creates a .profile repository with README for the organization
|
||||
func CreateProfileRepo(ctx *context.Context) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Check if user can create repos in this org
|
||||
if !ctx.Org.CanCreateOrgRepo {
|
||||
ctx.Flash.Error(ctx.Tr("org.profile_repo_no_permission"))
|
||||
ctx.Redirect(org.AsUser().HomeLink())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if .profile repo already exists
|
||||
exists, err := repo_model.IsRepositoryModelExist(ctx, org.AsUser(), ".profile")
|
||||
if err != nil {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
ctx.Redirect(org.AsUser().HomeLink() + "/.profile")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the .profile repository
|
||||
repo, err := repo_service.CreateRepository(ctx, ctx.Doer, org.AsUser(), repo_service.CreateRepoOptions{
|
||||
Name: ".profile",
|
||||
Description: "Organization profile",
|
||||
AutoInit: true,
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
IsPrivate: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CreateProfileRepo: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("org.profile_repo_create_failed"))
|
||||
ctx.Redirect(org.AsUser().HomeLink())
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to edit the README
|
||||
ctx.Redirect(repo.Link() + "/_edit/main/README.md")
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||
ctx.Data["IsHomepagePinned"] = ctx.Org.Organization.IsHomepagePinned
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
@@ -89,6 +90,14 @@ func SettingsPost(ctx *context.Context) {
|
||||
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
|
||||
}
|
||||
|
||||
// Handle homepage pinning (admin only)
|
||||
if ctx.Doer.IsAdmin {
|
||||
if err := org.SetHomepagePinned(ctx, form.IsHomepagePinned); err != nil {
|
||||
ctx.ServerError("SetHomepagePinned", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pages
|
||||
@@ -70,15 +70,19 @@ func getRepoFromRequest(ctx *context.Context) (*repo_model.Repository, *pages_mo
|
||||
return repo, config, nil
|
||||
}
|
||||
|
||||
// Parse subdomain: {repo}.{owner}.pages.{domain}
|
||||
// This is a simplified implementation
|
||||
// Parse subdomain: {repo}-{owner}.{domain}
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 4 {
|
||||
if len(parts) < 2 {
|
||||
return nil, nil, errors.New("invalid pages subdomain")
|
||||
}
|
||||
|
||||
repoName := parts[0]
|
||||
ownerName := parts[1]
|
||||
// First part is {repo}-{owner}
|
||||
repoOwner := strings.SplitN(parts[0], "-", 2)
|
||||
if len(repoOwner) != 2 {
|
||||
return nil, nil, errors.New("invalid pages subdomain format")
|
||||
}
|
||||
repoName := repoOwner[0]
|
||||
ownerName := repoOwner[1]
|
||||
|
||||
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
|
||||
128
routers/web/repo/pin.go
Normal file
128
routers/web/repo/pin.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Pin handles pinning a repo to user profile or organization
|
||||
func Pin(ctx *context.Context) {
|
||||
pinType := ctx.FormString("type")
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if redirectTo == "" {
|
||||
redirectTo = ctx.Repo.RepoLink
|
||||
}
|
||||
|
||||
switch pinType {
|
||||
case "user":
|
||||
if err := user_model.PinRepoToUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
|
||||
if user_model.IsErrPinnedRepoLimit(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_limit"))
|
||||
} else {
|
||||
log.Error("PinRepoToUser failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.success_profile"))
|
||||
}
|
||||
|
||||
case "org":
|
||||
if !ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is a member of the org
|
||||
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
log.Error("IsOrganizationMember failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Use CreateOrgPinnedRepo
|
||||
pinnedRepo := &organization.OrgPinnedRepo{
|
||||
OrgID: ctx.Repo.Repository.OwnerID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
if err := organization.CreateOrgPinnedRepo(ctx, pinnedRepo); err != nil {
|
||||
if _, ok := err.(organization.ErrOrgPinnedRepoAlreadyExist); ok {
|
||||
ctx.Flash.Info(ctx.Tr("repo.pin.already_pinned_org"))
|
||||
} else {
|
||||
log.Error("CreateOrgPinnedRepo failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.success_org"))
|
||||
}
|
||||
|
||||
default:
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
|
||||
}
|
||||
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
|
||||
// Unpin handles unpinning a repo from user profile or organization
|
||||
func Unpin(ctx *context.Context) {
|
||||
pinType := ctx.FormString("type")
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if redirectTo == "" {
|
||||
redirectTo = ctx.Repo.RepoLink
|
||||
}
|
||||
|
||||
switch pinType {
|
||||
case "user":
|
||||
if err := user_model.UnpinRepoFromUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
|
||||
log.Error("UnpinRepoFromUser failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_profile"))
|
||||
}
|
||||
|
||||
case "org":
|
||||
if !ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is a member of the org
|
||||
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
log.Error("IsOrganizationMember failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
if err := organization.DeleteOrgPinnedRepo(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID); err != nil {
|
||||
log.Error("DeleteOrgPinnedRepo failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_org"))
|
||||
}
|
||||
|
||||
default:
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
|
||||
}
|
||||
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
155
routers/web/repo/setting/license.go
Normal file
155
routers/web/repo/setting/license.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
const tplLicense templates.TplName = "repo/settings/license"
|
||||
|
||||
// LicenseInfo holds license information
|
||||
type LicenseInfo struct {
|
||||
Key string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// LicenseCategory holds a category of licenses
|
||||
type LicenseCategory struct {
|
||||
Category string
|
||||
Licenses []LicenseInfo
|
||||
}
|
||||
|
||||
// LicenseTypes defines all available license types
|
||||
var LicenseTypes = []LicenseCategory{
|
||||
{
|
||||
Category: "Permissive Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"MIT", "MIT License", "Most popular overall, minimal restrictions"},
|
||||
{"Apache-2.0", "Apache License 2.0", "Like MIT, plus explicit patent protection"},
|
||||
{"BSD-2-Clause", "BSD 2-Clause", "Similar to MIT"},
|
||||
{"BSD-3-Clause", "BSD 3-Clause", "BSD with no-endorsement rule"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Weak Copyleft Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"MPL-2.0", "Mozilla Public License 2.0", "Only modified files must remain open source"},
|
||||
{"LGPL-2.1", "LGPL 2.1", "Allows linking from proprietary software"},
|
||||
{"LGPL-3.0", "LGPL 3.0", "Allows linking from proprietary software"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Strong Copyleft Licenses",
|
||||
Licenses: []LicenseInfo{
|
||||
{"GPL-2.0", "GNU GPL v2", "Derivatives must also be GPL"},
|
||||
{"GPL-3.0", "GNU GPL v3", "Derivatives must be GPL, includes patent clauses"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Network/SaaS Copyleft",
|
||||
Licenses: []LicenseInfo{
|
||||
{"AGPL-3.0", "AGPL v3", "Like GPL, but closes the SaaS loophole"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Source-Available",
|
||||
Licenses: []LicenseInfo{
|
||||
{"BSL-1.0", "Business Source License", "Free to use, becomes open source later"},
|
||||
{"SSPL-1.0", "Server Side Public License", "Requires publishing entire service stack"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Category: "Public Domain",
|
||||
Licenses: []LicenseInfo{
|
||||
{"Unlicense", "Unlicense", "Public domain, no attribution required"},
|
||||
{"CC0-1.0", "CC0", "Public domain, internationally defensible"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// License shows the license settings page
|
||||
func License(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.license")
|
||||
ctx.Data["PageIsSettingsLicense"] = true
|
||||
ctx.Data["LicenseTypes"] = LicenseTypes
|
||||
ctx.HTML(http.StatusOK, tplLicense)
|
||||
}
|
||||
|
||||
// LicensePost handles license settings form submission
|
||||
func LicensePost(ctx *context.Context) {
|
||||
licenseType := ctx.FormString("license_type")
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
oldLicenseType := repo.LicenseType
|
||||
|
||||
// Update repo license type
|
||||
repo.LicenseType = licenseType
|
||||
if err := repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "license_type"); err != nil {
|
||||
ctx.ServerError("UpdateRepositoryCols", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create LICENSE.md file if license selected and different from before
|
||||
if licenseType != "" && licenseType != oldLicenseType {
|
||||
if err := createLicenseFile(ctx, repo, licenseType); err != nil {
|
||||
log.Error("Failed to create LICENSE.md: %v", err)
|
||||
ctx.Flash.Warning(ctx.Tr("repo.settings.license_file_error"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_saved"))
|
||||
}
|
||||
} else if licenseType == "" {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_cleared"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.license_saved"))
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/license")
|
||||
}
|
||||
|
||||
func createLicenseFile(ctx *context.Context, repo *repo_model.Repository, licenseType string) error {
|
||||
// Get license content from templates
|
||||
licenseContent, err := repo_module.GetLicense(licenseType, &repo_module.LicenseValues{
|
||||
Owner: repo.OwnerName,
|
||||
Email: ctx.Doer.Email,
|
||||
Repo: repo.Name,
|
||||
Year: time.Now().Format("2006"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetLicense: %w", err)
|
||||
}
|
||||
|
||||
// Create/update LICENSE.md using files service
|
||||
opts := &files_service.ChangeRepoFilesOptions{
|
||||
Message: fmt.Sprintf("Add LICENSE.md (%s)", licenseType),
|
||||
OldBranch: repo.DefaultBranch,
|
||||
NewBranch: repo.DefaultBranch,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "LICENSE.md",
|
||||
ContentReader: bytes.NewReader(licenseContent),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
// If file already exists, try to update it instead
|
||||
opts.Files[0].Operation = "update"
|
||||
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
@@ -41,6 +41,7 @@ func Pages(ctx *context.Context) {
|
||||
|
||||
// Generate subdomain
|
||||
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
|
||||
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
|
||||
|
||||
// Available templates
|
||||
ctx.Data["PagesTemplates"] = []string{"simple", "documentation", "product", "portfolio"}
|
||||
@@ -92,7 +93,8 @@ func PagesPost(ctx *context.Context) {
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
|
||||
return
|
||||
}
|
||||
_, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, domain)
|
||||
sslExternal := ctx.FormBool("ssl_external")
|
||||
_, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, domain, sslExternal)
|
||||
if err != nil {
|
||||
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_exists"))
|
||||
@@ -112,6 +114,14 @@ func PagesPost(ctx *context.Context) {
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_deleted"))
|
||||
|
||||
case "activate_ssl":
|
||||
domainID := ctx.FormInt64("domain_id")
|
||||
if err := repo_model.ActivatePagesDomainSSL(ctx, domainID); err != nil {
|
||||
ctx.ServerError("ActivatePagesDomainSSL", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ssl_activated"))
|
||||
|
||||
case "verify_domain":
|
||||
domainID := ctx.FormInt64("domain_id")
|
||||
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user