Compare commits
54 Commits
v1.26.6-gitcaddy
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -22,7 +22,7 @@ jobs:
|
|||||||
# Lint job - must pass
|
# Lint job - must pass
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
# Unit tests with SQLite (no external database needed)
|
# Unit tests with SQLite (no external database needed)
|
||||||
test-unit:
|
test-unit:
|
||||||
name: Unit Tests
|
name: Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
# Integration tests with PostgreSQL
|
# Integration tests with PostgreSQL
|
||||||
test-pgsql:
|
test-pgsql:
|
||||||
name: Integration Tests (PostgreSQL)
|
name: Integration Tests (PostgreSQL)
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
services:
|
services:
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
# Create release job - runs first to create the release before build jobs upload
|
# Create release job - runs first to create the release before build jobs upload
|
||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
outputs:
|
outputs:
|
||||||
release_id: ${{ steps.create.outputs.release_id }}
|
release_id: ${{ steps.create.outputs.release_id }}
|
||||||
@@ -197,7 +197,7 @@ jobs:
|
|||||||
# Build job for binaries
|
# Build job for binaries
|
||||||
build:
|
build:
|
||||||
name: Build Binaries
|
name: Build Binaries
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
needs: [lint, create-release]
|
needs: [lint, create-release]
|
||||||
if: always() && needs.lint.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
|
if: always() && needs.lint.result == 'success' && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped')
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// gitea-cli is a command-line tool for interacting with Gitea instances.
|
// gitea-cli is a command-line tool for interacting with Gitea instances.
|
||||||
|
|||||||
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/azcore v1.19.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2
|
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/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/ProtonMail/go-crypto v1.3.0
|
||||||
github.com/PuerkitoBio/goquery v1.10.3
|
github.com/PuerkitoBio/goquery v1.10.3
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
|
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
|
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/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||||
github.com/DataDog/zstd v1.5.7 // 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/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
||||||
github.com/STARRY-S/zip v0.2.3 // 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
|
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
|
// 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
|
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=
|
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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
|
||||||
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/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 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
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=
|
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 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
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/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 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
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=
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package actions
|
package actions
|
||||||
@@ -64,6 +64,8 @@ type ActionRunner struct {
|
|||||||
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
|
||||||
// CapabilitiesJSON stores structured capability information for AI consumption
|
// CapabilitiesJSON stores structured capability information for AI consumption
|
||||||
CapabilitiesJSON string `xorm:"TEXT"`
|
CapabilitiesJSON string `xorm:"TEXT"`
|
||||||
|
// BandwidthTestRequestedAt tracks when a bandwidth test was requested by admin
|
||||||
|
BandwidthTestRequestedAt timeutil.TimeStamp `xorm:"index"`
|
||||||
|
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package migrations
|
package migrations
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v1_26
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package organization
|
package organization
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package organization
|
package organization
|
||||||
@@ -144,10 +144,10 @@ func GetMemberPublicVisibility(ctx context.Context, orgID, userID int64) (bool,
|
|||||||
|
|
||||||
// OrgOverviewStats represents statistics for the organization overview
|
// OrgOverviewStats represents statistics for the organization overview
|
||||||
type OrgOverviewStats struct {
|
type OrgOverviewStats struct {
|
||||||
MemberCount int64
|
TotalRepos int64
|
||||||
RepoCount int64
|
TotalMembers int64
|
||||||
PublicRepoCount int64
|
TotalTeams int64
|
||||||
TeamCount int64
|
TotalStars int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrgMemberAndTeamCounts returns member and team counts for an organization
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
@@ -142,6 +142,12 @@ func UpdatePagesDomain(ctx context.Context, domain *PagesDomain) error {
|
|||||||
return err
|
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
|
// DeletePagesDomain deletes a pages domain
|
||||||
func DeletePagesDomain(ctx context.Context, id int64) error {
|
func DeletePagesDomain(ctx context.Context, id int64) error {
|
||||||
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
@@ -968,6 +968,17 @@ func CountNullArchivedRepository(ctx context.Context) (int64, error) {
|
|||||||
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
|
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
|
// FixNullArchivedRepository sets is_archived to false where it is null
|
||||||
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
|
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
|
||||||
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
@@ -147,9 +147,10 @@ type User struct {
|
|||||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
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
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package actions
|
package actions
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package circuitbreaker implements the circuit breaker pattern for external service calls.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package errors
|
package errors
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package errors
|
package errors
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package health provides comprehensive health checking for Gitea services.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package idempotency provides middleware for idempotent POST request handling.
|
// Package idempotency provides middleware for idempotent POST request handling.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package operations provides tracking for long-running operations
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package nuget
|
package nuget
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package pages
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package storage
|
package storage
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package structs
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
// RunnerCapability represents the detailed capabilities of a runner
|
// RunnerCapability represents the detailed capabilities of a runner
|
||||||
type RunnerCapability struct {
|
type RunnerCapability struct {
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
Arch string `json:"arch"`
|
Arch string `json:"arch"`
|
||||||
|
Distro *DistroInfo `json:"distro,omitempty"`
|
||||||
Docker bool `json:"docker"`
|
Docker bool `json:"docker"`
|
||||||
DockerCompose bool `json:"docker_compose"`
|
DockerCompose bool `json:"docker_compose"`
|
||||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||||
@@ -14,6 +40,9 @@ type RunnerCapability struct {
|
|||||||
Tools map[string][]string `json:"tools,omitempty"`
|
Tools map[string][]string `json:"tools,omitempty"`
|
||||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||||
Limitations []string `json:"limitations,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
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package structs // import "code.gitea.io/gitea/modules/structs"
|
package structs // import "code.gitea.io/gitea/modules/structs"
|
||||||
|
|||||||
@@ -179,10 +179,10 @@ type OrgOverview struct {
|
|||||||
|
|
||||||
// OrgOverviewStats represents organization statistics
|
// OrgOverviewStats represents organization statistics
|
||||||
type OrgOverviewStats struct {
|
type OrgOverviewStats struct {
|
||||||
MemberCount int64 `json:"member_count"`
|
TotalRepos int64 `json:"total_repos"`
|
||||||
RepoCount int64 `json:"repo_count"`
|
TotalMembers int64 `json:"total_members"`
|
||||||
PublicRepoCount int64 `json:"public_repo_count"`
|
TotalTeams int64 `json:"total_teams"`
|
||||||
TeamCount int64 `json:"team_count"`
|
TotalStars int64 `json:"total_stars"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrgProfileContent represents the organization profile content
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package structs
|
package structs
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package structs
|
package structs
|
||||||
@@ -38,6 +38,8 @@ type AddPagesDomainOption struct {
|
|||||||
// The custom domain to add
|
// The custom domain to add
|
||||||
// required: true
|
// required: true
|
||||||
Domain string `json:"domain" binding:"Required"`
|
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
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package structs
|
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.
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
@@ -46,10 +46,19 @@ func NewFuncMap() template.FuncMap {
|
|||||||
"PathEscapeSegments": util.PathEscapeSegments,
|
"PathEscapeSegments": util.PathEscapeSegments,
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
"StringUtils": NewStringUtils,
|
"StringUtils": NewStringUtils,
|
||||||
"SliceUtils": NewSliceUtils,
|
"SliceUtils": NewSliceUtils,
|
||||||
"JsonUtils": NewJsonUtils,
|
"newSlice": func() []any { return []any{} },
|
||||||
"DateUtils": NewDateUtils,
|
"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
|
// svg / avatar / icon / color
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package templates
|
package templates
|
||||||
@@ -33,3 +33,17 @@ func (su *SliceUtils) Contains(s, v any) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package templates
|
package templates
|
||||||
@@ -35,6 +35,7 @@ func (su *StringUtils) ToString(v any) string {
|
|||||||
func (su *StringUtils) HasPrefix(s, prefix string) bool {
|
func (su *StringUtils) HasPrefix(s, prefix string) bool {
|
||||||
return strings.HasPrefix(s, prefix)
|
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 {
|
func (su *StringUtils) Contains(s, substr string) bool {
|
||||||
return strings.Contains(s, substr)
|
return strings.Contains(s, substr)
|
||||||
@@ -61,6 +62,10 @@ func (su *StringUtils) ToUpper(s string) string {
|
|||||||
return strings.ToUpper(s)
|
return strings.ToUpper(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (su *StringUtils) ToLower(s string) string {
|
||||||
|
return strings.ToLower(s)
|
||||||
|
}
|
||||||
|
|
||||||
func (su *StringUtils) TrimPrefix(s, prefix string) string {
|
func (su *StringUtils) TrimPrefix(s, prefix string) string {
|
||||||
return strings.TrimPrefix(s, prefix)
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package validation
|
package validation
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package middleware
|
package middleware
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package middleware
|
package middleware
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package webhook
|
package webhook
|
||||||
|
|||||||
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package runner
|
package runner
|
||||||
@@ -115,10 +115,10 @@ func (s *Service) Declare(
|
|||||||
req *connect.Request[runnerv1.DeclareRequest],
|
req *connect.Request[runnerv1.DeclareRequest],
|
||||||
) (*connect.Response[runnerv1.DeclareResponse], error) {
|
) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||||
runner := GetRunner(ctx)
|
runner := GetRunner(ctx)
|
||||||
runner.AgentLabels = req.Msg.Labels
|
|
||||||
runner.Version = req.Msg.Version
|
runner.Version = req.Msg.Version
|
||||||
runner.CapabilitiesJSON = req.Msg.CapabilitiesJson
|
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)
|
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +141,15 @@ func (s *Service) FetchTask(
|
|||||||
) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
||||||
runner := GetRunner(ctx)
|
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
|
var task *runnerv1.Task
|
||||||
tasksVersion := req.Msg.TasksVersion // task version from runner
|
tasksVersion := req.Msg.TasksVersion // task version from runner
|
||||||
latestVersion, err := actions_model.GetTasksVersionByScope(ctx, runner.OwnerID, runner.RepoID)
|
latestVersion, err := actions_model.GetTasksVersionByScope(ctx, runner.OwnerID, runner.RepoID)
|
||||||
@@ -167,9 +176,22 @@ func (s *Service) FetchTask(
|
|||||||
task = t
|
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{
|
res := connect.NewResponse(&runnerv1.FetchTaskResponse{
|
||||||
Task: task,
|
Task: task,
|
||||||
TasksVersion: latestVersion,
|
TasksVersion: latestVersion,
|
||||||
|
RequestBandwidthTest: requestBandwidthTest,
|
||||||
})
|
})
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package v1 Gitea API
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package org
|
package org
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package org
|
package org
|
||||||
@@ -92,10 +92,10 @@ func GetOverview(ctx *context.APIContext) {
|
|||||||
PublicMembers: apiPublicMembers,
|
PublicMembers: apiPublicMembers,
|
||||||
TotalMembers: totalMembers,
|
TotalMembers: totalMembers,
|
||||||
Stats: &api.OrgOverviewStats{
|
Stats: &api.OrgOverviewStats{
|
||||||
MemberCount: stats.MemberCount,
|
TotalRepos: stats.TotalRepos,
|
||||||
RepoCount: stats.RepoCount,
|
TotalMembers: stats.TotalMembers,
|
||||||
PublicRepoCount: stats.PublicRepoCount,
|
TotalTeams: stats.TotalTeams,
|
||||||
TeamCount: stats.TeamCount,
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
@@ -219,7 +219,7 @@ func AddPagesDomain(ctx *context.APIContext) {
|
|||||||
|
|
||||||
form := web.GetForm(ctx).(*api.AddPagesDomainOption)
|
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 err != nil {
|
||||||
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, "Domain already exists")
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package v2 Gitea API v2
|
// Package v2 Gitea API v2
|
||||||
@@ -86,6 +86,9 @@ func Routes() *web.Router {
|
|||||||
m.Get("/component/{component}", ComponentHealthCheck)
|
m.Get("/component/{component}", ComponentHealthCheck)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// MCP Protocol endpoint for AI tool integration
|
||||||
|
m.Post("/mcp", MCPHandler)
|
||||||
|
|
||||||
// Operation progress endpoints (SSE)
|
// Operation progress endpoints (SSE)
|
||||||
m.Group("/operations", func() {
|
m.Group("/operations", func() {
|
||||||
m.Get("/{id}/progress", OperationProgress)
|
m.Get("/{id}/progress", OperationProgress)
|
||||||
@@ -140,6 +143,8 @@ func Routes() *web.Router {
|
|||||||
// Actions v2 API - AI-friendly runner capability discovery
|
// Actions v2 API - AI-friendly runner capability discovery
|
||||||
m.Group("/repos/{owner}/{repo}/actions", func() {
|
m.Group("/repos/{owner}/{repo}/actions", func() {
|
||||||
m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities)
|
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)
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
781
routers/api/v2/mcp.go
Normal file
781
routers/api/v2/mcp.go
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RawMessage is a raw encoded JSON value (equivalent to encoding/json.RawMessage)
|
||||||
|
type RawMessage []byte
|
||||||
|
|
||||||
|
// MCP Protocol Types (JSON-RPC 2.0)
|
||||||
|
|
||||||
|
type MCPRequest struct {
|
||||||
|
JSONRPC string `json:"jsonrpc"`
|
||||||
|
ID any `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
package v2
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package v2
|
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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package routers
|
package routers
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package org
|
package org
|
||||||
@@ -17,12 +17,21 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
org_service "code.gitea.io/gitea/services/org"
|
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"
|
const tplOrgHome templates.TplName = "org/home"
|
||||||
|
|
||||||
// Home show organization home page
|
// Home show organization home page
|
||||||
@@ -103,6 +112,43 @@ func home(ctx *context.Context, viewRepositories bool) {
|
|||||||
ctx.Data["Teams"] = ctx.Org.Teams
|
ctx.Data["Teams"] = ctx.Org.Teams
|
||||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
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)
|
prepareResult, err := shared_user.RenderUserOrgHeader(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -157,12 +203,10 @@ func home(ctx *context.Context, viewRepositories bool) {
|
|||||||
}
|
}
|
||||||
ctx.Data["OrgStats"] = orgStats
|
ctx.Data["OrgStats"] = orgStats
|
||||||
|
|
||||||
// if no profile readme, it still means "view repositories"
|
// Always show overview by default for organizations
|
||||||
isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult)
|
isViewOverview := !viewRepositories
|
||||||
// Also show overview if there are pinned repos even without profile readme
|
// Load profile readme if available
|
||||||
if !viewRepositories && len(pinnedRepos) > 0 {
|
prepareOrgProfileReadme(ctx, prepareResult)
|
||||||
isViewOverview = true
|
|
||||||
}
|
|
||||||
ctx.Data["PageIsViewRepositories"] = !isViewOverview
|
ctx.Data["PageIsViewRepositories"] = !isViewOverview
|
||||||
ctx.Data["PageIsViewOverview"] = isViewOverview
|
ctx.Data["PageIsViewOverview"] = isViewOverview
|
||||||
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
|
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
|
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
|
||||||
return true
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package pages
|
package pages
|
||||||
@@ -70,15 +70,19 @@ func getRepoFromRequest(ctx *context.Context) (*repo_model.Repository, *pages_mo
|
|||||||
return repo, config, nil
|
return repo, config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse subdomain: {repo}.{owner}.pages.{domain}
|
// Parse subdomain: {repo}-{owner}.{domain}
|
||||||
// This is a simplified implementation
|
|
||||||
parts := strings.Split(host, ".")
|
parts := strings.Split(host, ".")
|
||||||
if len(parts) < 4 {
|
if len(parts) < 2 {
|
||||||
return nil, nil, errors.New("invalid pages subdomain")
|
return nil, nil, errors.New("invalid pages subdomain")
|
||||||
}
|
}
|
||||||
|
|
||||||
repoName := parts[0]
|
// First part is {repo}-{owner}
|
||||||
ownerName := parts[1]
|
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)
|
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||||
if err != nil {
|
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 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
@@ -41,6 +41,7 @@ func Pages(ctx *context.Context) {
|
|||||||
|
|
||||||
// Generate subdomain
|
// Generate subdomain
|
||||||
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
|
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
|
||||||
|
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
|
||||||
|
|
||||||
// Available templates
|
// Available templates
|
||||||
ctx.Data["PagesTemplates"] = []string{"simple", "documentation", "product", "portfolio"}
|
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")
|
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
|
||||||
return
|
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 err != nil {
|
||||||
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
if repo_model.IsErrPagesDomainAlreadyExist(err) {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_exists"))
|
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"))
|
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":
|
case "verify_domain":
|
||||||
domainID := ctx.FormInt64("domain_id")
|
domainID := ctx.FormInt64("domain_id")
|
||||||
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {
|
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
// Copyright 2022 The Gitea Authors and MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@@ -15,6 +19,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
@@ -295,6 +300,45 @@ func ResetRunnerRegistrationToken(ctx *context.Context) {
|
|||||||
ctx.JSONRedirect(redirectTo)
|
ctx.JSONRedirect(redirectTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunnerRequestBandwidthTest handles admin request to trigger a bandwidth test
|
||||||
|
func RunnerRequestBandwidthTest(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerID := ctx.PathParamInt64("runnerid")
|
||||||
|
ownerID := rCtx.OwnerID
|
||||||
|
repoID := rCtx.RepoID
|
||||||
|
redirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
|
||||||
|
|
||||||
|
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("RunnerRequestBandwidthTest.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
|
||||||
|
ctx.ServerError("RunnerRequestBandwidthTest.GetRunnerByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !runner.EditableInContext(ownerID, repoID) {
|
||||||
|
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the bandwidth test request timestamp
|
||||||
|
runner.BandwidthTestRequestedAt = timeutil.TimeStampNow()
|
||||||
|
err = actions_model.UpdateRunner(ctx, runner, "bandwidth_test_requested_at")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("RunnerRequestBandwidthTest.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
|
||||||
|
ctx.Flash.Warning(ctx.Tr("actions.runners.bandwidth_test_request_failed"))
|
||||||
|
ctx.Redirect(redirectTo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("RunnerRequestBandwidthTest success: %s", ctx.Req.URL)
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.runners.bandwidth_test_requested"))
|
||||||
|
ctx.Redirect(redirectTo)
|
||||||
|
}
|
||||||
|
|
||||||
// RunnerDeletePost response for deleting runner
|
// RunnerDeletePost response for deleting runner
|
||||||
func RunnerDeletePost(ctx *context.Context) {
|
func RunnerDeletePost(ctx *context.Context) {
|
||||||
rCtx, err := getRunnersCtx(ctx)
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
@@ -368,3 +412,306 @@ func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.Ac
|
|||||||
|
|
||||||
return got[0]
|
return got[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunnerAddLabel adds a single label to a runner
|
||||||
|
func RunnerAddLabel(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runner := findActionsRunner(ctx, rCtx)
|
||||||
|
if runner == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
label := ctx.FormString("label")
|
||||||
|
if label == "" {
|
||||||
|
ctx.Flash.Warning("No label specified")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if label already exists
|
||||||
|
if slices.Contains(runner.AgentLabels, label) {
|
||||||
|
ctx.Flash.Info("Label already exists")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the label
|
||||||
|
runner.AgentLabels = append(runner.AgentLabels, label)
|
||||||
|
|
||||||
|
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("RunnerAddLabel.UpdateRunner failed: %v", err)
|
||||||
|
ctx.Flash.Warning("Failed to add label")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success("Label added: " + label)
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerRemoveLabel removes a single label from a runner
|
||||||
|
func RunnerRemoveLabel(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runner := findActionsRunner(ctx, rCtx)
|
||||||
|
if runner == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
label := ctx.FormString("label")
|
||||||
|
if label == "" {
|
||||||
|
ctx.Flash.Warning("No label specified")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the label
|
||||||
|
newLabels := make([]string, 0, len(runner.AgentLabels))
|
||||||
|
found := false
|
||||||
|
for _, existing := range runner.AgentLabels {
|
||||||
|
if existing == label {
|
||||||
|
found = true
|
||||||
|
} else {
|
||||||
|
newLabels = append(newLabels, existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
ctx.Flash.Info("Label not found")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.AgentLabels = newLabels
|
||||||
|
|
||||||
|
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("RunnerRemoveLabel.UpdateRunner failed: %v", err)
|
||||||
|
ctx.Flash.Warning("Failed to remove label")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success("Label removed: " + label)
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerUseSuggestedLabels adds all suggested labels based on capabilities
|
||||||
|
func RunnerUseSuggestedLabels(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runner := findActionsRunner(ctx, rCtx)
|
||||||
|
if runner == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse capabilities to get suggested labels
|
||||||
|
if runner.CapabilitiesJSON == "" {
|
||||||
|
ctx.Flash.Warning("No capabilities data available")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var caps structs.RunnerCapability
|
||||||
|
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps); err != nil {
|
||||||
|
ctx.Flash.Warning("Failed to parse capabilities")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build suggested labels
|
||||||
|
suggestedLabels := []string{}
|
||||||
|
existingSet := make(map[string]bool)
|
||||||
|
for _, label := range runner.AgentLabels {
|
||||||
|
existingSet[label] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS-based labels
|
||||||
|
switch caps.OS {
|
||||||
|
case "linux":
|
||||||
|
suggestedLabels = append(suggestedLabels, "linux", "linux-latest")
|
||||||
|
case "windows":
|
||||||
|
suggestedLabels = append(suggestedLabels, "windows", "windows-latest")
|
||||||
|
case "darwin":
|
||||||
|
suggestedLabels = append(suggestedLabels, "macos", "macos-latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distro-based labels
|
||||||
|
if caps.Distro != nil && caps.Distro.ID != "" {
|
||||||
|
suggestedLabels = append(suggestedLabels, caps.Distro.ID, caps.Distro.ID+"-latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add only new labels
|
||||||
|
added := []string{}
|
||||||
|
for _, label := range suggestedLabels {
|
||||||
|
if !existingSet[label] {
|
||||||
|
runner.AgentLabels = append(runner.AgentLabels, label)
|
||||||
|
added = append(added, label)
|
||||||
|
existingSet[label] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(added) == 0 {
|
||||||
|
ctx.Flash.Info("All suggested labels already exist")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("RunnerUseSuggestedLabels.UpdateRunner failed: %v", err)
|
||||||
|
ctx.Flash.Warning("Failed to add labels")
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success("Added labels: " + strings.Join(added, ", "))
|
||||||
|
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerStatusJSON returns runner status as JSON for AJAX polling
|
||||||
|
func RunnerStatusJSON(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runner := findActionsRunner(ctx, rCtx)
|
||||||
|
if runner == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse capabilities
|
||||||
|
var caps *structs.RunnerCapability
|
||||||
|
if runner.CapabilitiesJSON != "" {
|
||||||
|
caps = &structs.RunnerCapability{}
|
||||||
|
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), caps); err != nil {
|
||||||
|
caps = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response matching the tile structure
|
||||||
|
response := map[string]any{
|
||||||
|
"id": runner.ID,
|
||||||
|
"name": runner.Name,
|
||||||
|
"is_online": runner.IsOnline(),
|
||||||
|
"status": runner.StatusLocaleName(ctx.Locale),
|
||||||
|
"version": runner.Version,
|
||||||
|
"labels": runner.AgentLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
if runner.LastOnline.AsTime().Unix() > 0 {
|
||||||
|
response["last_online"] = runner.LastOnline.AsTime().Format("2006-01-02T15:04:05Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
if caps != nil {
|
||||||
|
if caps.Disk != nil {
|
||||||
|
response["disk"] = map[string]any{
|
||||||
|
"total_bytes": caps.Disk.Total,
|
||||||
|
"free_bytes": caps.Disk.Free,
|
||||||
|
"used_bytes": caps.Disk.Used,
|
||||||
|
"used_percent": caps.Disk.UsedPercent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if caps.Bandwidth != nil {
|
||||||
|
bw := map[string]any{
|
||||||
|
"download_mbps": caps.Bandwidth.DownloadMbps,
|
||||||
|
"latency_ms": caps.Bandwidth.Latency,
|
||||||
|
}
|
||||||
|
if !caps.Bandwidth.TestedAt.IsZero() {
|
||||||
|
bw["tested_at"] = caps.Bandwidth.TestedAt.Format("2006-01-02T15:04:05Z")
|
||||||
|
}
|
||||||
|
response["bandwidth"] = bw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnersStatusJSON returns status for all runners as JSON for AJAX polling on the list page
|
||||||
|
func RunnersStatusJSON(ctx *context.Context) {
|
||||||
|
rCtx, err := getRunnersCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getRunnersCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := actions_model.FindRunnerOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if rCtx.IsRepo {
|
||||||
|
opts.RepoID = rCtx.RepoID
|
||||||
|
opts.WithAvailable = true
|
||||||
|
} else if rCtx.IsOrg || rCtx.IsUser {
|
||||||
|
opts.OwnerID = rCtx.OwnerID
|
||||||
|
opts.WithAvailable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
runners, _, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FindRunners", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make([]map[string]any, 0, len(runners))
|
||||||
|
for _, runner := range runners {
|
||||||
|
item := map[string]any{
|
||||||
|
"id": runner.ID,
|
||||||
|
"is_online": runner.IsOnline(),
|
||||||
|
"status": runner.StatusLocaleName(ctx.Locale),
|
||||||
|
"version": runner.Version,
|
||||||
|
}
|
||||||
|
if runner.LastOnline.AsTime().Unix() > 0 {
|
||||||
|
item["last_online"] = runner.LastOnline.AsTime().Format("2006-01-02T15:04:05Z")
|
||||||
|
// Calculate relative time
|
||||||
|
duration := time.Since(runner.LastOnline.AsTime())
|
||||||
|
var relativeTime string
|
||||||
|
if duration < time.Minute {
|
||||||
|
relativeTime = "just now"
|
||||||
|
} else if duration < time.Hour {
|
||||||
|
mins := int(duration.Minutes())
|
||||||
|
if mins == 1 {
|
||||||
|
relativeTime = "1 minute ago"
|
||||||
|
} else {
|
||||||
|
relativeTime = fmt.Sprintf("%d minutes ago", mins)
|
||||||
|
}
|
||||||
|
} else if duration < 24*time.Hour {
|
||||||
|
hours := int(duration.Hours())
|
||||||
|
if hours == 1 {
|
||||||
|
relativeTime = "1 hour ago"
|
||||||
|
} else {
|
||||||
|
relativeTime = fmt.Sprintf("%d hours ago", hours)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
days := int(duration.Hours() / 24)
|
||||||
|
if days == 1 {
|
||||||
|
relativeTime = "1 day ago"
|
||||||
|
} else {
|
||||||
|
relativeTime = fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item["last_online_relative"] = relativeTime
|
||||||
|
}
|
||||||
|
response = append(response, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// Copyright 2015 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
@@ -79,15 +79,10 @@ func userProfile(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
|
func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
|
||||||
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
|
// Default to overview page for users
|
||||||
// if there is not a profile readme, the overview tab should be treated as the repositories tab
|
|
||||||
tab := ctx.FormString("tab")
|
tab := ctx.FormString("tab")
|
||||||
if tab == "" || tab == "overview" {
|
if tab == "" {
|
||||||
if profileReadme != nil {
|
tab = "overview"
|
||||||
tab = "overview"
|
|
||||||
} else {
|
|
||||||
tab = "repositories"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.Data["TabName"] = tab
|
ctx.Data["TabName"] = tab
|
||||||
ctx.Data["HasUserProfileReadme"] = profileReadme != nil
|
ctx.Data["HasUserProfileReadme"] = profileReadme != nil
|
||||||
@@ -252,16 +247,46 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
|
|
||||||
total = int(count)
|
total = int(count)
|
||||||
case "overview":
|
case "overview":
|
||||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
// Load heatmap if user has it enabled
|
||||||
log.Error("failed to GetBlobContent: %v", err)
|
if ctx.ContextUser.ShowHeatmapOnProfile && setting.Service.EnableUserHeatmap {
|
||||||
} else {
|
data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
|
if err != nil {
|
||||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
log.Error("GetUserHeatmapDataByUser: %v", err)
|
||||||
})
|
|
||||||
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
|
||||||
log.Error("failed to RenderString: %v", err)
|
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["ProfileReadmeContent"] = profileContent
|
ctx.Data["HeatmapData"] = data
|
||||||
|
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load pinned repositories
|
||||||
|
pinnedRepos, err := user_model.GetPinnedRepos(ctx, ctx.ContextUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetPinnedRepos: %v", err)
|
||||||
|
} else {
|
||||||
|
// Load repo details for each pinned repo
|
||||||
|
for _, p := range pinnedRepos {
|
||||||
|
repo, err := repo_model.GetRepositoryByID(ctx, p.RepoID)
|
||||||
|
if err == nil {
|
||||||
|
p.Repo = repo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["UserPinnedRepos"] = pinnedRepos
|
||||||
|
ctx.Data["IsContextUserProfile"] = ctx.Doer != nil && ctx.Doer.ID == ctx.ContextUser.ID
|
||||||
|
|
||||||
|
// Load profile README
|
||||||
|
if profileReadme != nil {
|
||||||
|
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
||||||
|
log.Error("failed to GetBlobContent: %v", err)
|
||||||
|
} else {
|
||||||
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
|
||||||
|
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||||
|
})
|
||||||
|
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
||||||
|
log.Error("failed to RenderString: %v", err)
|
||||||
|
} else {
|
||||||
|
ctx.Data["ProfileReadmeContent"] = profileContent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "organizations":
|
case "organizations":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package setting
|
package setting
|
||||||
@@ -97,12 +97,13 @@ func ProfilePost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts := &user_service.UpdateOptions{
|
opts := &user_service.UpdateOptions{
|
||||||
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
|
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
|
||||||
Description: optional.Some(form.Description),
|
Description: optional.Some(form.Description),
|
||||||
Website: optional.Some(form.Website),
|
Website: optional.Some(form.Website),
|
||||||
Location: optional.Some(form.Location),
|
Location: optional.Some(form.Location),
|
||||||
Visibility: optional.Some(form.Visibility),
|
Visibility: optional.Some(form.Visibility),
|
||||||
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
||||||
|
ShowHeatmapOnProfile: optional.Some(form.ShowHeatmapOnProfile),
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.FullName != "" {
|
if form.FullName != "" {
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package web
|
package web
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
@@ -302,6 +303,44 @@ var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{Disable
|
|||||||
|
|
||||||
// registerWebRoutes register routes
|
// registerWebRoutes register routes
|
||||||
func registerWebRoutes(m *web.Router) {
|
func registerWebRoutes(m *web.Router) {
|
||||||
|
// Check for Pages subdomain and custom domain requests first
|
||||||
|
m.Use(func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
host := req.Host
|
||||||
|
// Remove port if present
|
||||||
|
if idx := strings.Index(host, ":"); idx > 0 {
|
||||||
|
host = host[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a subdomain of our main domain
|
||||||
|
mainDomain := setting.Domain
|
||||||
|
if subdomain, found := strings.CutSuffix(host, "."+mainDomain); found {
|
||||||
|
// Skip known subdomains
|
||||||
|
if subdomain != "" && subdomain != "www" && subdomain != "api" && subdomain != "git" && strings.Contains(subdomain, "-") {
|
||||||
|
// This looks like a Pages subdomain ({repo}-{owner})
|
||||||
|
ctx := context.GetWebContext(req.Context())
|
||||||
|
if ctx != nil {
|
||||||
|
log.Trace("Pages subdomain request: %s", host)
|
||||||
|
pages.ServeLandingPage(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if host != mainDomain && host != "www."+mainDomain {
|
||||||
|
// Check if this is a custom domain for Pages
|
||||||
|
domain, err := repo_model.GetPagesDomainByDomain(req.Context(), host)
|
||||||
|
if err == nil && domain != nil && domain.Verified {
|
||||||
|
ctx := context.GetWebContext(req.Context())
|
||||||
|
if ctx != nil {
|
||||||
|
log.Trace("Pages custom domain request: %s -> repo %d", host, domain.RepoID)
|
||||||
|
pages.ServeLandingPage(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// required to be signed in or signed out
|
// required to be signed in or signed out
|
||||||
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
|
||||||
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
|
||||||
@@ -470,6 +509,12 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
|
m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
|
||||||
Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
|
Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
|
||||||
m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
|
m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
|
||||||
|
m.Post("/{runnerid}/bandwidth-test", shared_actions.RunnerRequestBandwidthTest)
|
||||||
|
m.Post("/{runnerid}/add-label", shared_actions.RunnerAddLabel)
|
||||||
|
m.Post("/{runnerid}/remove-label", shared_actions.RunnerRemoveLabel)
|
||||||
|
m.Post("/{runnerid}/use-suggested-labels", shared_actions.RunnerUseSuggestedLabels)
|
||||||
|
m.Get("/status", shared_actions.RunnersStatusJSON)
|
||||||
|
m.Get("/{runnerid}/status", shared_actions.RunnerStatusJSON)
|
||||||
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
|
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -908,6 +953,7 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
|
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||||
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
|
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||||
m.Post("/members/action/{action}", org.MembersAction)
|
m.Post("/members/action/{action}", org.MembersAction)
|
||||||
|
m.Post("/create-profile-repo", org.CreateProfileRepo)
|
||||||
m.Get("/teams", org.Teams)
|
m.Get("/teams", org.Teams)
|
||||||
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
|
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
|
||||||
|
|
||||||
@@ -1670,6 +1716,8 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
||||||
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
||||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||||
|
m.Get("/action/pin", reqSignIn, repo.Pin)
|
||||||
|
m.Get("/action/unpin", reqSignIn, repo.Unpin)
|
||||||
}, optSignIn, context.RepoAssignment)
|
}, optSignIn, context.RepoAssignment)
|
||||||
|
|
||||||
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
|
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
// Package gitea provides a Go SDK for the Gitea API.
|
// Package gitea provides a Go SDK for the Gitea API.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package gitea
|
package gitea
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package attachment
|
package attachment
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
// Copyright 2016 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package context
|
package context
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// Copyright 2014 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package context
|
package context
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
@@ -415,6 +416,21 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
|||||||
|
|
||||||
ctx.Repo.Repository = repo
|
ctx.Repo.Repository = repo
|
||||||
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
||||||
|
|
||||||
|
// Check if repo is pinned (for pin dropdown)
|
||||||
|
if ctx.Doer != nil {
|
||||||
|
isPinnedToUser, _ := user_model.IsRepoPinnedByUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
|
ctx.Data["IsRepoPinnedToUser"] = isPinnedToUser
|
||||||
|
}
|
||||||
|
if ctx.Repo.Repository.Owner.IsOrganization() {
|
||||||
|
isPinnedToOrg, _ := organization.IsRepoPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||||
|
ctx.Data["IsRepoPinnedToOrg"] = isPinnedToOrg
|
||||||
|
// Check if user is a member of the org for pin dropdown
|
||||||
|
if ctx.Doer != nil {
|
||||||
|
isMember, _ := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
|
||||||
|
ctx.Data["IsOrganizationMember"] = isMember
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors and MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package convert
|
package convert
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
// Copyright 2020 The Gitea Authors and MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package cron
|
package cron
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
// 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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package forms
|
package forms
|
||||||
@@ -211,14 +211,15 @@ func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) b
|
|||||||
|
|
||||||
// UpdateProfileForm form for updating profile
|
// UpdateProfileForm form for updating profile
|
||||||
type UpdateProfileForm struct {
|
type UpdateProfileForm struct {
|
||||||
Name string `binding:"Username;MaxSize(40)"`
|
Name string `binding:"Username;MaxSize(40)"`
|
||||||
FullName string `binding:"MaxSize(100)"`
|
FullName string `binding:"MaxSize(100)"`
|
||||||
KeepEmailPrivate bool
|
KeepEmailPrivate bool
|
||||||
Website string `binding:"ValidSiteUrl;MaxSize(255)"`
|
Website string `binding:"ValidSiteUrl;MaxSize(255)"`
|
||||||
Location string `binding:"MaxSize(50)"`
|
Location string `binding:"MaxSize(50)"`
|
||||||
Description string `binding:"MaxSize(255)"`
|
Description string `binding:"MaxSize(255)"`
|
||||||
Visibility structs.VisibleType
|
Visibility structs.VisibleType
|
||||||
KeepActivityPrivate bool
|
KeepActivityPrivate bool
|
||||||
|
ShowHeatmapOnProfile bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package org
|
package org
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetOrgPinnedReposWithDetails returns all pinned repos with repo and group details loaded
|
// GetOrgPinnedReposWithDetails returns all pinned repos with repo and group details loaded
|
||||||
@@ -38,9 +37,13 @@ func GetOrgPinnedReposWithDetails(ctx context.Context, orgID int64) ([]*organiza
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach repos
|
// Attach repos and load attributes (including primary language)
|
||||||
for _, p := range pinnedRepos {
|
for _, p := range pinnedRepos {
|
||||||
p.Repo = repos[p.RepoID]
|
repo := repos[p.RepoID]
|
||||||
|
if repo != nil {
|
||||||
|
_ = repo.LoadAttributes(ctx)
|
||||||
|
}
|
||||||
|
p.Repo = repo
|
||||||
}
|
}
|
||||||
|
|
||||||
return pinnedRepos, nil
|
return pinnedRepos, nil
|
||||||
@@ -54,23 +57,22 @@ func GetOrgOverviewStats(ctx context.Context, orgID int64) (*organization.OrgOve
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
stats.MemberCount = memberCount
|
stats.TotalMembers = memberCount
|
||||||
stats.TeamCount = teamCount
|
stats.TotalTeams = teamCount
|
||||||
|
|
||||||
// Repo counts
|
// Repo count
|
||||||
stats.RepoCount, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
|
stats.TotalRepos, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
|
||||||
OwnerID: orgID,
|
OwnerID: orgID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.PublicRepoCount, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
|
// Total stars across all repos
|
||||||
OwnerID: orgID,
|
stats.TotalStars, err = repo_model.CountOrgRepoStars(ctx, orgID)
|
||||||
Private: optional.Some(false),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Non-fatal, just log and continue
|
||||||
|
stats.TotalStars = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats, nil
|
return stats, nil
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package pages
|
package pages
|
||||||
@@ -44,6 +44,10 @@ func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_mo
|
|||||||
}
|
}
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
// If Pages is enabled but no config file, return a default config
|
||||||
|
if dbConfig != nil && dbConfig.Enabled {
|
||||||
|
return getDefaultConfig(repo, string(dbConfig.Template)), nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +97,25 @@ func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadConfigFromRepo loads the landing.yaml configuration from the repository
|
// loadConfigFromRepo loads the landing.yaml configuration from the repository
|
||||||
|
|
||||||
|
// getDefaultConfig returns a default landing page configuration
|
||||||
|
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
|
||||||
|
if template == "" {
|
||||||
|
template = "simple"
|
||||||
|
}
|
||||||
|
return &pages_module.LandingConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Template: template,
|
||||||
|
Hero: pages_module.HeroConfig{
|
||||||
|
Title: repo.Name,
|
||||||
|
Tagline: repo.Description,
|
||||||
|
},
|
||||||
|
Branding: pages_module.BrandingConfig{
|
||||||
|
PrimaryColor: "#4183c4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
|
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
|
||||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,16 +195,17 @@ func DisablePages(ctx context.Context, repo *repo_model.Repository) error {
|
|||||||
|
|
||||||
// GetPagesSubdomain returns the subdomain for a repository's pages
|
// GetPagesSubdomain returns the subdomain for a repository's pages
|
||||||
func GetPagesSubdomain(repo *repo_model.Repository) string {
|
func GetPagesSubdomain(repo *repo_model.Repository) string {
|
||||||
// Format: {repo}.{owner}.pages.{domain}
|
// Format: {repo}-{owner}.{domain}
|
||||||
return fmt.Sprintf("%s.%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
|
return fmt.Sprintf("%s-%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPagesURL returns the full URL for a repository's pages
|
// GetPagesURL returns the full URL for a repository's pages
|
||||||
func GetPagesURL(repo *repo_model.Repository) string {
|
func GetPagesURL(repo *repo_model.Repository) string {
|
||||||
subdomain := GetPagesSubdomain(repo)
|
subdomain := GetPagesSubdomain(repo)
|
||||||
// This should be configurable
|
// This should be configurable
|
||||||
pagesDomain := setting.AppURL // TODO: Add proper pages domain setting
|
// Extract domain from settings
|
||||||
return fmt.Sprintf("https://%s.pages.%s", subdomain, pagesDomain)
|
domain := setting.Domain
|
||||||
|
return fmt.Sprintf("https://%s.%s", subdomain, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPagesDomains returns all custom domains for a repository's pages
|
// GetPagesDomains returns all custom domains for a repository's pages
|
||||||
@@ -190,7 +214,7 @@ func GetPagesDomains(ctx context.Context, repoID int64) ([]*repo_model.PagesDoma
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddPagesDomain adds a custom domain for pages
|
// AddPagesDomain adds a custom domain for pages
|
||||||
func AddPagesDomain(ctx context.Context, repoID int64, domain string) (*repo_model.PagesDomain, error) {
|
func AddPagesDomain(ctx context.Context, repoID int64, domain string, sslExternal bool) (*repo_model.PagesDomain, error) {
|
||||||
// Normalize domain
|
// Normalize domain
|
||||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||||
|
|
||||||
@@ -200,9 +224,15 @@ func AddPagesDomain(ctx context.Context, repoID int64, domain string) (*repo_mod
|
|||||||
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
|
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sslStatus := repo_model.SSLStatusPending
|
||||||
|
if sslExternal {
|
||||||
|
sslStatus = repo_model.SSLStatusActive
|
||||||
|
}
|
||||||
|
|
||||||
pagesDomain := &repo_model.PagesDomain{
|
pagesDomain := &repo_model.PagesDomain{
|
||||||
RepoID: repoID,
|
RepoID: repoID,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
|
SSLStatus: sslStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {
|
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {
|
||||||
|
|||||||
@@ -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
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package release
|
package release
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package user
|
package user
|
||||||
@@ -47,6 +47,7 @@ type UpdateOptions struct {
|
|||||||
IsRestricted optional.Option[bool]
|
IsRestricted optional.Option[bool]
|
||||||
Visibility optional.Option[structs.VisibleType]
|
Visibility optional.Option[structs.VisibleType]
|
||||||
KeepActivityPrivate optional.Option[bool]
|
KeepActivityPrivate optional.Option[bool]
|
||||||
|
ShowHeatmapOnProfile optional.Option[bool]
|
||||||
Language optional.Option[string]
|
Language optional.Option[string]
|
||||||
Theme optional.Option[string]
|
Theme optional.Option[string]
|
||||||
DiffViewStyle optional.Option[string]
|
DiffViewStyle optional.Option[string]
|
||||||
@@ -158,6 +159,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
|||||||
|
|
||||||
cols = append(cols, "keep_activity_private")
|
cols = append(cols, "keep_activity_private")
|
||||||
}
|
}
|
||||||
|
if opts.ShowHeatmapOnProfile.Has() {
|
||||||
|
u.ShowHeatmapOnProfile = opts.ShowHeatmapOnProfile.Value()
|
||||||
|
|
||||||
|
cols = append(cols, "show_heatmap_on_profile")
|
||||||
|
}
|
||||||
|
|
||||||
if opts.AllowCreateOrganization.Has() {
|
if opts.AllowCreateOrganization.Has() {
|
||||||
u.AllowCreateOrganization = opts.AllowCreateOrganization.Value()
|
u.AllowCreateOrganization = opts.AllowCreateOrganization.Value()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
// Copyright 2026 MarketAlly. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package wiki
|
package wiki
|
||||||
|
|||||||
@@ -4,119 +4,191 @@
|
|||||||
|
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui mobile reversed stackable grid">
|
<div class="ui mobile reversed stackable grid">
|
||||||
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
|
<div class="ui eleven wide column">
|
||||||
|
{{/* Profile README Section */}}
|
||||||
{{if .ProfileReadmeContent}}
|
{{if .ProfileReadmeContent}}
|
||||||
<div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div>
|
<div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* Pinned Repositories Section */}}
|
{{/* Overview Tab Content */}}
|
||||||
{{if and .PageIsViewOverview .HasPinnedRepos}}
|
{{if .PageIsViewOverview}}
|
||||||
<div class="ui segment pinned-repos-section">
|
{{/* Pinned Repositories Section */}}
|
||||||
<h4 class="ui header">
|
<div class="ui segment pinned-repos-section">
|
||||||
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "org.pinned_repos"}}
|
<h4 class="ui header tw-flex tw-items-center">
|
||||||
</h4>
|
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "org.pinned_repos"}}
|
||||||
|
{{if .IsOrganizationOwner}}
|
||||||
|
<a class="tw-ml-auto ui mini button" href="{{.OrgLink}}/settings">
|
||||||
|
{{svg "octicon-gear" 14}} {{ctx.Locale.Tr "org.settings.pinned.manage"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</h4>
|
||||||
|
|
||||||
{{/* Ungrouped pinned repos */}}
|
{{if .HasPinnedRepos}}
|
||||||
{{if .UngroupedPinned}}
|
{{/* Ungrouped pinned repos */}}
|
||||||
<div class="ui three stackable cards pinned-repos">
|
{{if .UngroupedPinned}}
|
||||||
{{range .UngroupedPinned}}
|
<div class="ui three stackable cards pinned-repos">
|
||||||
{{if .Repo}}
|
{{range .UngroupedPinned}}
|
||||||
<a class="ui card" href="{{.Repo.Link}}">
|
{{if .Repo}}
|
||||||
<div class="content">
|
<a class="ui card" href="{{.Repo.Link}}">
|
||||||
<div class="header text truncate">
|
<div class="content tw-text-center">
|
||||||
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 16}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 16}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 16}}{{else}}{{svg "octicon-repo" 16}}{{end}}
|
{{if .Repo.Avatar}}
|
||||||
{{.Repo.Name}}
|
<img class="tw-inline-block tw-rounded" style="max-width: 80px; max-height: 80px; object-fit: contain;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
|
||||||
|
{{else}}
|
||||||
|
<div class="tw-inline-block tw-p-4">
|
||||||
|
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="header tw-mt-2">{{.Repo.Name}}</div>
|
||||||
|
{{if .Repo.Description}}
|
||||||
|
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="extra content">
|
||||||
|
{{if .Repo.PrimaryLanguage}}
|
||||||
|
<span class="tw-mr-2">
|
||||||
|
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
|
||||||
|
{{.Repo.PrimaryLanguage.Language}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
{{if .Repo.NumStars}}
|
||||||
|
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
|
||||||
|
{{end}}
|
||||||
|
{{if .Repo.NumForks}}
|
||||||
|
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Grouped pinned repos */}}
|
||||||
|
{{range .PinnedGroups}}
|
||||||
|
{{$groupRepos := index $.PinnedByGroup .ID}}
|
||||||
|
{{if $groupRepos}}
|
||||||
|
<div class="pinned-group tw-mt-4">
|
||||||
|
<h5 class="ui header tw-mb-2">
|
||||||
|
{{svg "octicon-chevron-down" 14}} {{.Name}}
|
||||||
|
</h5>
|
||||||
|
<div class="ui three stackable cards pinned-repos">
|
||||||
|
{{range $groupRepos}}
|
||||||
|
{{if .Repo}}
|
||||||
|
<a class="ui card" href="{{.Repo.Link}}">
|
||||||
|
<div class="content tw-text-center">
|
||||||
|
{{if .Repo.Avatar}}
|
||||||
|
<img class="tw-inline-block tw-rounded" style="max-width: 80px; max-height: 80px; object-fit: contain;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
|
||||||
|
{{else}}
|
||||||
|
<div class="tw-inline-block tw-p-4">
|
||||||
|
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="header tw-mt-2">{{.Repo.Name}}</div>
|
||||||
|
{{if .Repo.Description}}
|
||||||
|
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="extra content">
|
||||||
|
{{if .Repo.PrimaryLanguage}}
|
||||||
|
<span class="tw-mr-2">
|
||||||
|
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
|
||||||
|
{{.Repo.PrimaryLanguage.Language}}
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
{{if .Repo.NumStars}}
|
||||||
|
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
|
||||||
|
{{end}}
|
||||||
|
{{if .Repo.NumForks}}
|
||||||
|
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
{{/* Empty state for pinned repos */}}
|
||||||
|
<div class="ui placeholder segment tw-text-center">
|
||||||
|
<div class="ui icon header">
|
||||||
|
{{svg "octicon-pin" 48}}
|
||||||
|
<div class="content">
|
||||||
|
{{ctx.Locale.Tr "org.pinned_repos_empty_title"}}
|
||||||
|
<div class="sub header">
|
||||||
|
{{ctx.Locale.Tr "org.pinned_repos_empty_desc"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .IsOrganizationOwner}}
|
||||||
|
<div class="tw-mt-4">
|
||||||
|
<a class="ui primary button" href="{{.OrgLink}}/settings">
|
||||||
|
{{svg "octicon-gear" 16}} {{ctx.Locale.Tr "org.settings.pinned.setup"}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{if .Repo.Description}}
|
|
||||||
<div class="description text truncate">{{.Repo.Description}}</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="extra content">
|
{{end}}
|
||||||
{{if .Repo.PrimaryLanguage}}
|
</div>
|
||||||
<span class="tw-mr-2">
|
|
||||||
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
|
{{/* Profile README Empty State */}}
|
||||||
{{.Repo.PrimaryLanguage.Language}}
|
{{if and (not .ProfileReadmeContent) .IsOrganizationOwner}}
|
||||||
</span>
|
<div class="ui segment tw-mt-4">
|
||||||
{{end}}
|
<div class="ui placeholder segment tw-text-center">
|
||||||
{{if .Repo.NumStars}}
|
<div class="ui icon header">
|
||||||
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
|
{{svg "octicon-book" 32}}
|
||||||
{{end}}
|
<div class="content">
|
||||||
{{if .Repo.NumForks}}
|
{{ctx.Locale.Tr "org.profile_readme_empty_title"}}
|
||||||
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
|
<div class="sub header">
|
||||||
{{end}}
|
{{ctx.Locale.Tr "org.profile_readme_empty_desc"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<div class="tw-mt-4">
|
||||||
{{end}}
|
<form action="{{.OrgLink}}/create-profile-repo" method="post">{{.CsrfTokenHtml}}<button class="ui primary button" type="submit">
|
||||||
{{end}}
|
{{svg "octicon-plus" 16}} {{ctx.Locale.Tr "org.create_profile_repo"}}
|
||||||
|
</button></form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* Grouped pinned repos */}}
|
{{/* Recent Activity Section */}}
|
||||||
{{range .PinnedGroups}}
|
{{if .RecentActivity}}
|
||||||
{{$groupRepos := index $.PinnedByGroup .ID}}
|
<div class="ui segment tw-mt-4">
|
||||||
{{if $groupRepos}}
|
<h4 class="ui header tw-flex tw-items-center">
|
||||||
<div class="pinned-group tw-mt-4">
|
{{svg "octicon-pulse" 16}} {{ctx.Locale.Tr "org.recent_activity"}}
|
||||||
<h5 class="ui header tw-mb-2">
|
</h4>
|
||||||
{{svg "octicon-chevron-down" 14}} {{.Name}}
|
<div class="ui relaxed divided list">
|
||||||
</h5>
|
{{range .RecentActivity}}
|
||||||
<div class="ui three stackable cards pinned-repos">
|
<div class="item">
|
||||||
{{range $groupRepos}}
|
<div class="tw-flex tw-items-center tw-gap-3">
|
||||||
{{if .Repo}}
|
{{if .Repo.Avatar}}
|
||||||
<a class="ui card" href="{{.Repo.Link}}">
|
<img style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
|
||||||
<div class="content">
|
{{else}}
|
||||||
<div class="header text truncate">
|
<div class="tw-w-8 tw-h-8 tw-flex tw-items-center tw-justify-center">
|
||||||
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 16}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 16}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 16}}{{else}}{{svg "octicon-repo" 16}}{{end}}
|
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 20}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 20}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 20}}{{else}}{{svg "octicon-repo" 20}}{{end}}
|
||||||
{{.Repo.Name}}
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="tw-flex-1 tw-min-w-0">
|
||||||
|
<a href="{{.Repo.Link}}" class="tw-font-semibold">{{.Repo.Name}}</a>
|
||||||
|
{{if .CommitMessage}}
|
||||||
|
<p class="text grey tw-text-sm tw-truncate tw-mb-0">{{.CommitMessage}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-right tw-text-sm text grey tw-flex-shrink-0">
|
||||||
|
<span title="{{DateUtils.FullTime .CommitTime}}">{{DateUtils.TimeSince .CommitTime}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Repo.Description}}
|
|
||||||
<div class="description text truncate">{{.Repo.Description}}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="extra content">
|
</div>
|
||||||
{{if .Repo.PrimaryLanguage}}
|
|
||||||
<span class="tw-mr-2">
|
|
||||||
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
|
|
||||||
{{.Repo.PrimaryLanguage.Language}}
|
|
||||||
</span>
|
|
||||||
{{end}}
|
|
||||||
{{if .Repo.NumStars}}
|
|
||||||
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
|
|
||||||
{{end}}
|
|
||||||
{{if .Repo.NumForks}}
|
|
||||||
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{/* Public Members Section (on overview) */}}
|
|
||||||
{{if and .PageIsViewOverview .PublicMembers}}
|
|
||||||
<div class="ui segment public-members-section tw-mt-4">
|
|
||||||
<h4 class="ui header tw-flex tw-items-center">
|
|
||||||
{{svg "octicon-people" 16}} {{ctx.Locale.Tr "org.public_members"}}
|
|
||||||
{{if .HasMorePublicMembers}}
|
|
||||||
<a class="tw-ml-auto text grey tw-text-sm" href="{{.OrgLink}}/members">{{ctx.Locale.Tr "org.view_all_members" .TotalPublicMembers}}</a>
|
|
||||||
{{end}}
|
|
||||||
</h4>
|
|
||||||
<div class="tw-flex tw-flex-wrap tw-gap-2">
|
|
||||||
{{range .PublicMembers}}
|
|
||||||
<a href="{{.User.HomeLink}}" title="{{.User.Name}} ({{.Role}})" class="tw-flex tw-flex-col tw-items-center tw-p-2">
|
|
||||||
{{ctx.AvatarUtils.Avatar .User 48}}
|
|
||||||
<span class="tw-text-sm tw-mt-1">{{.User.Name}}</span>
|
|
||||||
<span class="tw-text-xs text grey">{{.Role}}</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Repositories Tab Content */}}
|
||||||
{{if .PageIsViewRepositories}}
|
{{if .PageIsViewRepositories}}
|
||||||
{{template "shared/repo/search" .}}
|
{{template "shared/repo/search" .}}
|
||||||
{{template "shared/repo/list" .}}
|
{{template "shared/repo/list" .}}
|
||||||
@@ -124,7 +196,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .ShowMemberAndTeamTab}}
|
|
||||||
<div class="ui five wide column">
|
<div class="ui five wide column">
|
||||||
{{if .CanCreateOrgRepo}}
|
{{if .CanCreateOrgRepo}}
|
||||||
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-x-1 tw-gap-y-2 tw-mb-4">
|
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-x-1 tw-gap-y-2 tw-mb-4">
|
||||||
@@ -136,7 +207,7 @@
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if and .ShowMemberAndTeamTab .ShowOrgProfileReadmeSelector}}
|
{{if .ShowOrgProfileReadmeSelector}}
|
||||||
<div class="tw-my-4">
|
<div class="tw-my-4">
|
||||||
<div id="org-home-view-as-dropdown" class="ui dropdown jump">
|
<div id="org-home-view-as-dropdown" class="ui dropdown jump">
|
||||||
{{- $viewAsRole := Iif (.IsViewingOrgAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
|
{{- $viewAsRole := Iif (.IsViewingOrgAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
|
||||||
@@ -151,28 +222,56 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-my-2">
|
<div class="tw-my-2 text grey">
|
||||||
{{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}}
|
{{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .NumMembers}}
|
{{/* Organization Stats - Sidebar Card */}}
|
||||||
<h4 class="ui top attached header tw-flex">
|
{{if .OrgStats}}
|
||||||
|
<div class="ui top attached header tw-flex">
|
||||||
|
<strong class="tw-flex-1">{{svg "octicon-graph" 16}} {{ctx.Locale.Tr "org.stats"}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||||
|
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
|
||||||
|
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalRepos}}</div>
|
||||||
|
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.repositories"}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
|
||||||
|
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalMembers}}</div>
|
||||||
|
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.members"}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
|
||||||
|
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalTeams}}</div>
|
||||||
|
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.teams"}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
|
||||||
|
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalStars}}</div>
|
||||||
|
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "repo.stars"}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Members/Public Members Section */}}
|
||||||
|
{{if .IsOrganizationMember}}
|
||||||
|
{{/* Internal view - show all members */}}
|
||||||
|
{{if .NumMembers}}
|
||||||
|
<h4 class="ui top attached header tw-flex tw-mt-4">
|
||||||
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
|
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
|
||||||
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
|
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached segment members">
|
<div class="ui attached segment members">
|
||||||
{{$isMember := .IsOrganizationMember}}
|
|
||||||
{{range .Members}}
|
{{range .Members}}
|
||||||
{{if or $isMember (call $.IsPublicMember .ID)}}
|
<a href="{{.HomeLink}}" title="{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
|
||||||
<a href="{{.HomeLink}}" title="{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .IsOrganizationMember}}
|
|
||||||
<div class="ui top attached header tw-flex">
|
{{/* Teams - only for members */}}
|
||||||
|
<div class="ui top attached header tw-flex tw-mt-4">
|
||||||
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
|
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
|
||||||
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
|
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,9 +291,30 @@
|
|||||||
<a class="ui primary small button" href="{{.OrgLink}}/teams/new">{{ctx.Locale.Tr "org.create_new_team"}}</a>
|
<a class="ui primary small button" href="{{.OrgLink}}/teams/new">{{ctx.Locale.Tr "org.create_new_team"}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
{{/* Public view - show public members only */}}
|
||||||
|
{{if .PublicMembers}}
|
||||||
|
<h4 class="ui top attached header tw-flex tw-mt-4">
|
||||||
|
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.public_members"}}</strong>
|
||||||
|
{{if .HasMorePublicMembers}}
|
||||||
|
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.TotalPublicMembers}}</span> {{svg "octicon-chevron-right"}}</a>
|
||||||
|
{{end}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment members">
|
||||||
|
{{range .PublicMembers}}
|
||||||
|
<a href="{{.User.HomeLink}}" title="{{.User.Name}}{{if .User.FullName}} ({{.User.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar .User 48}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<h4 class="ui top attached header tw-flex tw-mt-4">
|
||||||
|
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.public_members"}}</strong>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<p class="text grey tw-text-center">{{ctx.Locale.Tr "org.no_public_members"}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
|
<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
|
||||||
<div class="overflow-menu-items">
|
<div class="overflow-menu-items">
|
||||||
{{if .HasOrgProfileReadme}}
|
<a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}">
|
||||||
<a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}">
|
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
|
||||||
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
|
</a>
|
||||||
</a>
|
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}/-/repositories">
|
||||||
{{end}}
|
|
||||||
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasOrgProfileReadme}}/-/repositories{{end}}">
|
|
||||||
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
|
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
|
||||||
{{if .RepoCount}}
|
{{if .RepoCount}}
|
||||||
<div class="ui small label">{{.RepoCount}}</div>
|
<div class="ui small label">{{.RepoCount}}</div>
|
||||||
|
|||||||
2
templates/pages/base_footer.tmpl
Normal file
2
templates/pages/base_footer.tmpl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
</body>
|
||||||
|
</html>
|
||||||
170
templates/pages/base_head.tmpl
Normal file
170
templates/pages/base_head.tmpl
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{if .Config.Hero.Title}}{{.Config.Hero.Title}}{{else}}{{.Repository.Name}}{{end}} - {{.Repository.Owner.Name}}</title>
|
||||||
|
<meta name="description" content="{{if .Config.Hero.Tagline}}{{.Config.Hero.Tagline}}{{else}}{{.Repository.Description}}{{end}}">
|
||||||
|
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
|
||||||
|
{{template "base/head_style" .}}
|
||||||
|
<style>
|
||||||
|
/* Pages standalone styles - no Gitea navbar */
|
||||||
|
:root {
|
||||||
|
--pages-primary: {{if .Config.Branding.PrimaryColor}}{{.Config.Branding.PrimaryColor}}{{else}}#4183c4{{end}};
|
||||||
|
--pages-secondary: {{if .Config.Branding.SecondaryColor}}{{.Config.Branding.SecondaryColor}}{{else}}#6c757d{{end}};
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body.pages-body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.pages-landing {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.pages-main { flex: 1; }
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
/* Header styles */
|
||||||
|
.pages-header {
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
padding: 16px 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.pages-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.pages-nav-brand {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
.pages-nav-logo { height: 32px; }
|
||||||
|
.pages-nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.pages-nav-link {
|
||||||
|
color: #586069;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.pages-nav-link:hover { color: var(--pages-primary); }
|
||||||
|
/* Hero styles */
|
||||||
|
.pages-hero {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #f6f8fa 0%, #fff 100%);
|
||||||
|
}
|
||||||
|
.pages-logo {
|
||||||
|
max-height: 80px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.pages-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
.pages-tagline {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #586069;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.pages-cta { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
|
||||||
|
/* Stats */
|
||||||
|
.pages-stats {
|
||||||
|
padding: 48px 0;
|
||||||
|
background: #f6f8fa;
|
||||||
|
}
|
||||||
|
.pages-stats-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 48px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.pages-stat {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.pages-stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
.pages-stat-label {
|
||||||
|
color: #586069;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
/* README */
|
||||||
|
.pages-readme {
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
.pages-readme .markup {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
/* Footer */
|
||||||
|
.pages-footer {
|
||||||
|
background: #24292e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 48px 0 24px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.pages-footer-links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.pages-footer-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.pages-footer-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pages-footer-list li { margin-bottom: 8px; }
|
||||||
|
.pages-footer-list a {
|
||||||
|
color: #959da5;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.pages-footer-list a:hover { color: #fff; }
|
||||||
|
.pages-footer-bottom {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #444d56;
|
||||||
|
}
|
||||||
|
.pages-footer-copyright, .pages-footer-powered {
|
||||||
|
color: #959da5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.pages-footer-powered a { color: #79b8ff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="pages-body">
|
||||||
@@ -1,5 +1,118 @@
|
|||||||
{{template "base/head" .}}
|
{{template "pages/base_head" .}}
|
||||||
<div class="page-content pages-landing pages-documentation">
|
<style>
|
||||||
|
/* Documentation-specific styles */
|
||||||
|
.pages-documentation {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.pages-documentation .pages-header {
|
||||||
|
background: #24292e;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.pages-documentation .pages-nav-brand,
|
||||||
|
.pages-documentation .pages-nav-link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.pages-documentation .pages-nav-link:hover {
|
||||||
|
color: #79b8ff;
|
||||||
|
}
|
||||||
|
.pages-docs-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: calc(100vh - 65px);
|
||||||
|
}
|
||||||
|
.pages-docs-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-right: 1px solid #e1e4e8;
|
||||||
|
padding: 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 65px;
|
||||||
|
height: calc(100vh - 65px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.pages-docs-search {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.pages-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #e1e4e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.pages-docs-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.pages-docs-section-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #586069;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.pages-docs-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pages-docs-list li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.pages-docs-list a {
|
||||||
|
color: #24292e;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.pages-docs-list a:hover {
|
||||||
|
background: #e1e4e8;
|
||||||
|
color: var(--pages-primary);
|
||||||
|
}
|
||||||
|
.pages-docs-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 48px;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
.pages-docs-article {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.pages-docs-article h1,
|
||||||
|
.pages-docs-article h2,
|
||||||
|
.pages-docs-article h3 {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
}
|
||||||
|
.pages-docs-article code {
|
||||||
|
background: #f6f8fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
.pages-docs-article pre {
|
||||||
|
background: #24292e;
|
||||||
|
color: #e1e4e8;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.pages-docs-article pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pages-docs-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.pages-docs-content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="pages-landing pages-documentation">
|
||||||
{{template "pages/header" .}}
|
{{template "pages/header" .}}
|
||||||
|
|
||||||
<div class="pages-docs-layout">
|
<div class="pages-docs-layout">
|
||||||
@@ -21,6 +134,15 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="pages-docs-section">
|
||||||
|
<h4 class="pages-docs-section-title">Documentation</h4>
|
||||||
|
<ul class="pages-docs-list">
|
||||||
|
<li><a href="#">Getting Started</a></li>
|
||||||
|
<li><a href="#">Installation</a></li>
|
||||||
|
<li><a href="#">Configuration</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -32,12 +154,11 @@
|
|||||||
{{.ReadmeContent}}
|
{{.ReadmeContent}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>{{ctx.Locale.Tr "repo.no_desc"}}</p>
|
<h1>{{.Repository.Name}}</h1>
|
||||||
|
<p>{{if .Repository.Description}}{{.Repository.Description}}{{else}}Welcome to the documentation.{{end}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "pages/footer" .}}
|
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "pages/base_footer" .}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user