Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d55da332cf | |||
| a04e237f17 | |||
| e7cc4c120f | |||
| aa54f3834e | |||
| 4d93f23037 | |||
| d7e2ffd02b | |||
| b9d81ca5c5 | |||
| 59c62dc687 | |||
| b14ffa083e | |||
| 2cc57bbbbc | |||
| 3cd7687c06 | |||
| c3b2643b0c | |||
| 0159e567e2 | |||
| f194b204b4 | |||
| f118f084ce | |||
| 2821c35326 | |||
| 5b02cf188e | |||
| 689173ecab | |||
| b2fe44fbc3 | |||
| 0e89ef9944 | |||
| 522dadecf0 | |||
| f1b9bb2f3d | |||
| 7bbaf218d5 | |||
| 33a550f838 | |||
| e29ee5f91b | |||
| 984a99188e | |||
| 92fc77a6d1 | |||
| ea411e09be | |||
| 9b141b39c5 | |||
| 85e4356fce | |||
| 1654181a9e | |||
| 282ef8f3e7 | |||
| a34eb53b2a | |||
| 75d53c11b4 | |||
| 8556314468 | |||
| 22624d662c | |||
| 91646c505b | |||
| b994fcdb9a | |||
| 6dc2c1dec7 | |||
| 4372e956de | |||
| a61cdbe2f1 | |||
| ac4092fbab | |||
| 30197e4e97 | |||
| 12132486a0 | |||
| 3f29562938 | |||
| 4a931dddab | |||
| c6f42487b5 | |||
| b101a2304a | |||
| 381952f6d2 | |||
| 1c667d9da9 | |||
| a88e3f8787 | |||
| 4012f3bea9 | |||
| 095b78b2a7 | |||
| ff72cd0cb0 | |||
| 50454db3fb | |||
| eab36f26aa | |||
| 4ce332d031 | |||
| 1b1ad35df4 | |||
| 426cffc224 | |||
| 0716ad0edd | |||
| 0572e6a164 | |||
| 4e51f48285 | |||
| aa56925bba | |||
| fc895aa70d | |||
| 1db8435737 | |||
| 71a486b534 | |||
| 90b9af6e3e | |||
| a99af91ab4 | |||
| 0eb81f9c1a | |||
| 6498459e49 |
@@ -4,7 +4,7 @@
|
||||
<name>MokoGitea</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
||||
<version>05.07.00</version>
|
||||
<version>05.13.00</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
noop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 05.07.00
|
||||
# VERSION: 05.13.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+161
-161
@@ -1,161 +1,161 @@
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
|
||||
@@ -113,6 +113,11 @@ func ListLicenseKeysByPackage(ctx context.Context, packageID int64) ([]*LicenseK
|
||||
return keys, db.GetEngine(ctx).Where("package_id = ?", packageID).Find(&keys)
|
||||
}
|
||||
|
||||
// CountKeysByPackage returns the number of keys for a package.
|
||||
func CountKeysByPackage(ctx context.Context, packageID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("package_id = ?", packageID).Count(new(LicenseKey))
|
||||
}
|
||||
|
||||
// UpdateLicenseKey updates a license key.
|
||||
func UpdateLicenseKey(ctx context.Context, key *LicenseKey) error {
|
||||
_, err := db.GetEngine(ctx).ID(key.ID).AllCols().Update(key)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -55,6 +57,20 @@ func GetLicensePackageByID(ctx context.Context, id int64) (*LicensePackage, erro
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
// FindLicensePackageOptions for db.Find/db.Count.
|
||||
type FindLicensePackageOptions struct {
|
||||
db.ListOptions
|
||||
OwnerID int64
|
||||
}
|
||||
|
||||
func (opts FindLicensePackageOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// ListLicensePackages returns all packages for the given owner.
|
||||
func ListLicensePackages(ctx context.Context, ownerID int64) ([]*LicensePackage, error) {
|
||||
pkgs := make([]*LicensePackage, 0, 10)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
)
|
||||
|
||||
const (
|
||||
MasterPackageName = "Master (Internal)"
|
||||
MasterPackageDesc = "Auto-created master package with unlimited access to all channels."
|
||||
)
|
||||
|
||||
// EnsureMasterKey ensures that a master license package and key exist for the given owner.
|
||||
// Returns the master key's raw key string only if it was just created (empty string otherwise).
|
||||
func EnsureMasterKey(ctx context.Context, ownerID int64) (rawKey string, err error) {
|
||||
// Check if a master package already exists.
|
||||
pkgs, err := ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var masterPkg *LicensePackage
|
||||
for _, pkg := range pkgs {
|
||||
if pkg.Name == MasterPackageName {
|
||||
masterPkg = pkg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create master package if it doesn't exist.
|
||||
if masterPkg == nil {
|
||||
masterPkg = &LicensePackage{
|
||||
OwnerID: ownerID,
|
||||
Name: MasterPackageName,
|
||||
Description: MasterPackageDesc,
|
||||
DurationDays: 0, // lifetime
|
||||
MaxSites: 0, // unlimited
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
if err := CreateLicensePackage(ctx, masterPkg); err != nil {
|
||||
return "", fmt.Errorf("CreateLicensePackage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a master key already exists for this package.
|
||||
keys, err := ListLicenseKeysByPackage(ctx, masterPkg.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if key.IsInternal {
|
||||
return "", nil // already exists, don't return raw key
|
||||
}
|
||||
}
|
||||
|
||||
// Create the master key.
|
||||
masterKey := &LicenseKey{
|
||||
PackageID: masterPkg.ID,
|
||||
OwnerID: ownerID,
|
||||
IsInternal: true,
|
||||
IsActive: true,
|
||||
}
|
||||
rawKey, err = CreateLicenseKey(ctx, masterKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("CreateLicenseKey: %w", err)
|
||||
}
|
||||
|
||||
return rawKey, nil
|
||||
}
|
||||
|
||||
// GetMasterKey returns the master key for an owner, if it exists.
|
||||
func GetMasterKey(ctx context.Context, ownerID int64) (*LicenseKey, error) {
|
||||
key := new(LicenseKey)
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND is_internal = ?", ownerID, true).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package licenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(UpdateStreamConfig))
|
||||
}
|
||||
|
||||
// UpdateStreamConfig stores update stream settings at org or repo level.
|
||||
// When OwnerID is set and RepoID is 0, it's an org-level default.
|
||||
// When RepoID is set, it's a per-repo override.
|
||||
type UpdateStreamConfig struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user
|
||||
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
|
||||
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, custom
|
||||
// CustomStreams is a JSON array of stream definitions.
|
||||
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
|
||||
CustomStreams string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (UpdateStreamConfig) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// StreamDef defines a single update stream/channel.
|
||||
type StreamDef struct {
|
||||
Name string `json:"name"` // e.g. "stable", "lts", "nightly"
|
||||
Suffix string `json:"suffix"` // tag suffix to match, e.g. "-lts", "-rc"
|
||||
Description string `json:"description"` // human-readable label
|
||||
}
|
||||
|
||||
// DefaultJoomlaStreams returns the standard Joomla update streams.
|
||||
func DefaultJoomlaStreams() []StreamDef {
|
||||
return []StreamDef{
|
||||
{Name: "stable", Suffix: "", Description: "Stable releases"},
|
||||
{Name: "release-candidate", Suffix: "-rc", Description: "Release candidates"},
|
||||
{Name: "beta", Suffix: "-beta", Description: "Beta testing"},
|
||||
{Name: "alpha", Suffix: "-alpha", Description: "Alpha / early access"},
|
||||
{Name: "development", Suffix: "-dev", Description: "Development builds"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomStreams parses the CustomStreams JSON field.
|
||||
func (c *UpdateStreamConfig) GetCustomStreams() []StreamDef {
|
||||
if c.CustomStreams == "" {
|
||||
return nil
|
||||
}
|
||||
var streams []StreamDef
|
||||
if err := json.Unmarshal([]byte(c.CustomStreams), &streams); err != nil {
|
||||
return nil
|
||||
}
|
||||
return streams
|
||||
}
|
||||
|
||||
// GetActiveStreams returns the effective streams for this config.
|
||||
func (c *UpdateStreamConfig) GetActiveStreams() []StreamDef {
|
||||
if c.StreamMode == "custom" {
|
||||
if custom := c.GetCustomStreams(); len(custom) > 0 {
|
||||
return custom
|
||||
}
|
||||
}
|
||||
return DefaultJoomlaStreams()
|
||||
}
|
||||
|
||||
// GetOrgConfig returns the org-level update stream config.
|
||||
func GetOrgConfig(ctx context.Context, ownerID int64) (*UpdateStreamConfig, error) {
|
||||
cfg := new(UpdateStreamConfig)
|
||||
has, err := db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", ownerID).Get(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return &UpdateStreamConfig{OwnerID: ownerID, StreamMode: "joomla"}, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetRepoConfig returns the repo-level override, or nil if none exists.
|
||||
func GetRepoConfig(ctx context.Context, repoID int64) (*UpdateStreamConfig, error) {
|
||||
cfg := new(UpdateStreamConfig)
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetEffectiveStreams resolves the streams for a repo: repo override → org default → Joomla default.
|
||||
func GetEffectiveStreams(ctx context.Context, ownerID, repoID int64) []StreamDef {
|
||||
// Check repo-level override first.
|
||||
repoCfg, err := GetRepoConfig(ctx, repoID)
|
||||
if err == nil && repoCfg != nil {
|
||||
return repoCfg.GetActiveStreams()
|
||||
}
|
||||
|
||||
// Fall back to org-level config.
|
||||
orgCfg, err := GetOrgConfig(ctx, ownerID)
|
||||
if err == nil && orgCfg != nil {
|
||||
return orgCfg.GetActiveStreams()
|
||||
}
|
||||
|
||||
return DefaultJoomlaStreams()
|
||||
}
|
||||
|
||||
// SaveConfig creates or updates an update stream config.
|
||||
func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
|
||||
existing := new(UpdateStreamConfig)
|
||||
var has bool
|
||||
var err error
|
||||
if cfg.RepoID > 0 {
|
||||
has, err = db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
|
||||
} else {
|
||||
has, err = db.GetEngine(ctx).Where("owner_id = ? AND repo_id = 0", cfg.OwnerID).Get(existing)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
cfg.ID = existing.ID
|
||||
_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
|
||||
} else {
|
||||
_, err = db.GetEngine(ctx).Insert(cfg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MatchStreamFromTag determines which stream a tag belongs to based on the given stream definitions.
|
||||
func MatchStreamFromTag(tagName string, isPrerelease bool, streams []StreamDef) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
|
||||
// Check custom suffixes (longest match first to avoid "-rc" matching before "-rc-special").
|
||||
var bestMatch string
|
||||
bestLen := 0
|
||||
for _, s := range streams {
|
||||
if s.Suffix == "" {
|
||||
continue // stable/default stream handled below
|
||||
}
|
||||
if strings.Contains(lower, s.Suffix) && len(s.Suffix) > bestLen {
|
||||
bestMatch = s.Name
|
||||
bestLen = len(s.Suffix)
|
||||
}
|
||||
}
|
||||
if bestMatch != "" {
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
// If prerelease and no suffix matched, use the first prerelease stream.
|
||||
if isPrerelease {
|
||||
for _, s := range streams {
|
||||
if s.Suffix != "" {
|
||||
return s.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: first stream with empty suffix (stable).
|
||||
for _, s := range streams {
|
||||
if s.Suffix == "" {
|
||||
return s.Name
|
||||
}
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
@@ -413,6 +413,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(333, "Add require_2fa to user table for org enforcement", v1_27.AddRequire2FAToUser),
|
||||
newMigration(334, "Add actions user whitelist to protected branches", v1_27.AddActionsUserWhitelistToProtectedBranch),
|
||||
newMigration(335, "Add license key tables for update server", v1_27.AddLicenseKeyTables),
|
||||
newMigration(336, "Add update stream config table", v1_27.AddUpdateStreamConfigTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type updateStreamConfig336 struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
|
||||
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"`
|
||||
CustomStreams string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED"`
|
||||
}
|
||||
|
||||
func (updateStreamConfig336) TableName() string {
|
||||
return "update_stream_config"
|
||||
}
|
||||
|
||||
// AddUpdateStreamConfigTable creates the update_stream_config table.
|
||||
func AddUpdateStreamConfigTable(x *xorm.Engine) error {
|
||||
return x.Sync(new(updateStreamConfig336))
|
||||
}
|
||||
@@ -2144,6 +2144,10 @@
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default",
|
||||
"repo.settings.releases_desc": "Enable Repository Releases",
|
||||
"repo.settings.unit_visibility": "Visibility",
|
||||
"repo.settings.unit_visibility_private": "Private (follow repo visibility)",
|
||||
"repo.settings.unit_visibility_public": "Public (anyone can read)",
|
||||
"repo.settings.unit_visibility_releases_help": "Update feeds (updates.xml, dolibarr.json) are always accessible regardless of this setting. Set to Public to also show the releases page to anonymous visitors.",
|
||||
"repo.settings.packages_desc": "Enable Repository Packages Registry",
|
||||
"repo.settings.projects_desc": "Enable Projects",
|
||||
"repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)",
|
||||
@@ -2608,6 +2612,40 @@
|
||||
"repo.release.detail": "Release details",
|
||||
"repo.release.tags": "Tags",
|
||||
"repo.release.new_release": "New Release",
|
||||
"repo.release.update_feed": "Update Feed",
|
||||
"repo.licenses": "Licenses",
|
||||
"repo.licenses.packages": "License Packages",
|
||||
"repo.licenses.package_name": "Package",
|
||||
"repo.licenses.duration": "Duration",
|
||||
"repo.licenses.channels": "Channels",
|
||||
"repo.licenses.keys_issued": "Keys",
|
||||
"repo.licenses.status": "Status",
|
||||
"repo.licenses.lifetime": "Lifetime",
|
||||
"repo.licenses.days": "days",
|
||||
"repo.licenses.all_channels": "All channels",
|
||||
"repo.licenses.active": "Active",
|
||||
"repo.licenses.inactive": "Inactive",
|
||||
"repo.licenses.none": "No License Packages",
|
||||
"repo.licenses.none_desc": "License packages can be created via the API to gate access to update streams.",
|
||||
"repo.licenses.issued_keys": "Issued Keys",
|
||||
"repo.licenses.key_prefix": "Key",
|
||||
"repo.licenses.licensee": "Licensee",
|
||||
"repo.licenses.expires": "Expires",
|
||||
"repo.licenses.never": "Never",
|
||||
"repo.licenses.new_package": "New Package",
|
||||
"repo.licenses.description": "Description",
|
||||
"repo.licenses.max_sites": "Max Sites",
|
||||
"repo.licenses.channels_help": "Comma-separated channel names (e.g. stable,release-candidate). Leave empty for all channels.",
|
||||
"repo.licenses.create_package": "Create Package",
|
||||
"repo.licenses.package_created": "License package created successfully.",
|
||||
"repo.licenses.generate_key": "Generate Key",
|
||||
"repo.licenses.key_created": "License Key Created",
|
||||
"repo.licenses.key_created_copy": "Copy this key now. It will not be shown again.",
|
||||
"repo.licenses.revoke": "Revoke",
|
||||
"repo.licenses.key_revoked": "License key revoked.",
|
||||
"repo.licenses.master_key_created": "Master License Key Created",
|
||||
"repo.licenses.master_key_created_copy": "This is your organization master key with unlimited access to all update channels. Copy it now — it will not be shown again.",
|
||||
"repo.licenses.update_feeds": "Update Feed URLs",
|
||||
"repo.release.draft": "Draft",
|
||||
"repo.release.prerelease": "Pre-Release",
|
||||
"repo.release.stable": "Stable",
|
||||
|
||||
@@ -82,6 +82,7 @@ func CreateLicensePackage(ctx *context.APIContext) {
|
||||
MaxSites: form.MaxSites,
|
||||
RepoScope: form.RepoScope,
|
||||
AllowedChannels: form.AllowedChannels,
|
||||
IsActive: true,
|
||||
}
|
||||
if pkg.RepoScope == "" {
|
||||
pkg.RepoScope = "all"
|
||||
@@ -121,6 +122,7 @@ func CreateLicenseKey(ctx *context.APIContext) {
|
||||
LicenseeEmail: form.LicenseeEmail,
|
||||
DomainRestriction: form.DomainRestriction,
|
||||
MaxSites: form.MaxSites,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if form.StartsAt != nil {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplOrgLicenses templates.TplName = "org/licenses"
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// Licenses shows the org-level license packages and keys.
|
||||
func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
|
||||
org := ctx.Org.Organization
|
||||
ownerID := org.ID
|
||||
|
||||
// Auto-create master key if org owner.
|
||||
if ctx.Org.IsOwner {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
}
|
||||
if newMasterKey != "" {
|
||||
ctx.Data["NewMasterKey"] = newMasterKey
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(pkg.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgLicenses)
|
||||
}
|
||||
|
||||
// LicensesCreatePackage handles POST to create a new org-level license package.
|
||||
func LicensesCreatePackage(ctx *context.Context) {
|
||||
name := ctx.FormString("name")
|
||||
if name == "" {
|
||||
ctx.Flash.Error("Package name is required")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
|
||||
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
DurationDays: durationDays,
|
||||
MaxSites: maxSites,
|
||||
AllowedChannels: ctx.FormString("allowed_channels"),
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
|
||||
// LicensesGenerateKey handles POST to generate a key from an org package.
|
||||
func LicensesGenerateKey(ctx *context.Context) {
|
||||
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
|
||||
if packageID == 0 {
|
||||
ctx.Flash.Error("Invalid package")
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if pkg.DurationDays > 0 {
|
||||
expires := time.Now().AddDate(0, 0, pkg.DurationDays)
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-render with the new key shown.
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Org.IsOwner
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
ownerID := ctx.Org.Organization.ID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(p.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplOrgLicenses)
|
||||
}
|
||||
|
||||
// LicensesRevokeKey handles POST to revoke an org license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/-/licenses")
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
const tplLicenses templates.TplName = "repo/licenses"
|
||||
|
||||
// LicensePackageDisplay is used in templates.
|
||||
type LicensePackageDisplay struct {
|
||||
*licenses.LicensePackage
|
||||
KeyCount int64
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// Licenses shows the license packages and keys for a repo.
|
||||
func Licenses(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
|
||||
// Auto-create master package + key if admin and none exist.
|
||||
if ctx.Repo.Permission.IsAdmin() {
|
||||
newMasterKey, err := licenses.EnsureMasterKey(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("EnsureMasterKey", err)
|
||||
return
|
||||
}
|
||||
if newMasterKey != "" {
|
||||
ctx.Data["NewMasterKey"] = newMasterKey
|
||||
}
|
||||
}
|
||||
|
||||
pkgs, err := licenses.ListLicensePackages(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicensePackages", err)
|
||||
return
|
||||
}
|
||||
|
||||
var display []LicensePackageDisplay
|
||||
for _, pkg := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, pkg.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: pkg,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(pkg.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
|
||||
keys, err := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListLicenseKeys", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplLicenses)
|
||||
}
|
||||
|
||||
// LicensesCreatePackage handles POST to create a new license package.
|
||||
func LicensesCreatePackage(ctx *context.Context) {
|
||||
name := ctx.FormString("name")
|
||||
if name == "" {
|
||||
ctx.Flash.Error("Package name is required")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
durationDays, _ := strconv.Atoi(ctx.FormString("duration_days"))
|
||||
maxSites, _ := strconv.Atoi(ctx.FormString("max_sites"))
|
||||
|
||||
pkg := &licenses.LicensePackage{
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
Name: name,
|
||||
Description: ctx.FormString("description"),
|
||||
DurationDays: durationDays,
|
||||
MaxSites: maxSites,
|
||||
AllowedChannels: ctx.FormString("allowed_channels"),
|
||||
RepoScope: "all",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := licenses.CreateLicensePackage(ctx, pkg); err != nil {
|
||||
ctx.ServerError("CreateLicensePackage", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.package_created"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
|
||||
// LicensesGenerateKey handles POST to generate a new key from a package.
|
||||
func LicensesGenerateKey(ctx *context.Context) {
|
||||
packageID, _ := strconv.ParseInt(ctx.FormString("package_id"), 10, 64)
|
||||
if packageID == 0 {
|
||||
ctx.Flash.Error("Invalid package")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := licenses.GetLicensePackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicensePackageByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key := &licenses.LicenseKey{
|
||||
PackageID: packageID,
|
||||
OwnerID: ctx.Repo.Repository.OwnerID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Auto-calculate expiry from package duration.
|
||||
if pkg.DurationDays > 0 {
|
||||
expires := time.Now().AddDate(0, 0, pkg.DurationDays)
|
||||
key.ExpiresUnix = timeutil.TimeStamp(expires.Unix())
|
||||
}
|
||||
|
||||
rawKey, err := licenses.CreateLicenseKey(ctx, key)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.licenses")
|
||||
ctx.Data["PageIsLicenses"] = true
|
||||
ctx.Data["IsLicensesPage"] = true
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
ctx.Data["NewKeyCreated"] = rawKey
|
||||
|
||||
// Re-render the page with the new key displayed.
|
||||
ownerID := ctx.Repo.Repository.OwnerID
|
||||
pkgs, _ := licenses.ListLicensePackages(ctx, ownerID)
|
||||
var display []LicensePackageDisplay
|
||||
for _, p := range pkgs {
|
||||
count, _ := licenses.CountKeysByPackage(ctx, p.ID)
|
||||
display = append(display, LicensePackageDisplay{
|
||||
LicensePackage: p,
|
||||
KeyCount: count,
|
||||
Created: time.Unix(int64(p.CreatedUnix), 0),
|
||||
})
|
||||
}
|
||||
ctx.Data["LicensePackages"] = display
|
||||
keys, _ := licenses.ListLicenseKeys(ctx, ownerID)
|
||||
ctx.Data["LicenseKeys"] = keys
|
||||
|
||||
ctx.HTML(http.StatusOK, tplLicenses)
|
||||
}
|
||||
|
||||
// LicensesRevokeKey handles POST to revoke a license key.
|
||||
func LicensesRevokeKey(ctx *context.Context) {
|
||||
keyID := ctx.PathParamInt64("id")
|
||||
key, err := licenses.GetLicenseKeyByID(ctx, keyID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLicenseKeyByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
key.IsActive = false
|
||||
if err := licenses.UpdateLicenseKey(ctx, key); err != nil {
|
||||
ctx.ServerError("UpdateLicenseKey", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.licenses.key_revoked"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/licenses")
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
user_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
@@ -510,6 +511,17 @@ func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config c
|
||||
return repoUnit
|
||||
}
|
||||
|
||||
// applyUnitVisibility sets AnonymousAccessMode on a unit based on the form value.
|
||||
// Values: "" or "not-set" = none, "anonymous-read" = anonymous read.
|
||||
func applyUnitVisibility(unit *repo_model.RepoUnit, visibility string) {
|
||||
switch visibility {
|
||||
case "anonymous-read":
|
||||
unit.AnonymousAccessMode = perm.AccessModeRead
|
||||
default:
|
||||
unit.AnonymousAccessMode = perm.AccessModeNone
|
||||
}
|
||||
}
|
||||
|
||||
func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.RepoSettingForm)
|
||||
repo := ctx.Repo.Repository
|
||||
@@ -527,7 +539,9 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
|
||||
u := newRepoUnit(repo, unit_model.TypeCode, nil)
|
||||
applyUnitVisibility(&u, form.CodeVisibility)
|
||||
units = append(units, u)
|
||||
} else if !unit_model.TypeCode.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
|
||||
}
|
||||
@@ -544,7 +558,9 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
|
||||
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
|
||||
u := newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig))
|
||||
applyUnitVisibility(&u, form.WikiVisibility)
|
||||
units = append(units, u)
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
|
||||
} else {
|
||||
if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
|
||||
@@ -581,11 +597,13 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}))
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
|
||||
} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
|
||||
u := newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
|
||||
EnableTimetracker: form.EnableTimetracker,
|
||||
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
|
||||
EnableDependencies: form.EnableIssueDependencies,
|
||||
}))
|
||||
})
|
||||
applyUnitVisibility(&u, form.IssuesVisibility)
|
||||
units = append(units, u)
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
|
||||
} else {
|
||||
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
|
||||
@@ -605,7 +623,9 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
|
||||
u := newRepoUnit(repo, unit_model.TypeReleases, nil)
|
||||
applyUnitVisibility(&u, form.ReleasesVisibility)
|
||||
units = append(units, u)
|
||||
} else if !unit_model.TypeReleases.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ func validateUpdateKey(ctx *context.Context) (allowedChannels []string, ok bool)
|
||||
channels = parsed
|
||||
}
|
||||
}
|
||||
// Normalize shorthand names to full Joomla convention.
|
||||
for i := range channels {
|
||||
channels[i] = updateserver.NormalizeChannel(channels[i])
|
||||
}
|
||||
return channels, true
|
||||
}
|
||||
|
||||
|
||||
@@ -1099,6 +1099,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
// at the moment, only editing "owner-level projects" need to "mention", maybe in the future we can relax the permission check
|
||||
m.Get("/mentions-in-owner", reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), org.GetMentionsInOwner)
|
||||
|
||||
m.Group("/licenses", func() {
|
||||
m.Get("", org.Licenses)
|
||||
m.Post("/packages", org.LicensesCreatePackage)
|
||||
m.Post("/keys/generate", org.LicensesGenerateKey)
|
||||
m.Post("/keys/{id}/revoke", org.LicensesRevokeKey)
|
||||
})
|
||||
|
||||
m.Get("/repositories", org.Repositories)
|
||||
m.Get("/heatmap", user.DashboardHeatmap)
|
||||
|
||||
@@ -1501,6 +1508,15 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": update server
|
||||
|
||||
// "/{username}/{reponame}": licenses page
|
||||
m.Group("/{username}/{reponame}/licenses", func() {
|
||||
m.Get("", repo.Licenses)
|
||||
m.Post("/packages", repo.LicensesCreatePackage)
|
||||
m.Post("/keys/generate", repo.LicensesGenerateKey)
|
||||
m.Post("/keys/{id}/revoke", repo.LicensesRevokeKey)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
// end "/{username}/{reponame}": licenses
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
|
||||
m.Get("/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
git_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
issues_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
|
||||
licenses_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
access_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm/access"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
unit_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
@@ -605,6 +606,14 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
||||
return
|
||||
}
|
||||
|
||||
// Check if license packages exist for this repo's owner (enables Licenses tab).
|
||||
numLicensePackages, _ := db.Count[licenses_model.LicensePackage](ctx, licenses_model.FindLicensePackageOptions{
|
||||
OwnerID: repo.OwnerID,
|
||||
})
|
||||
ctx.Data["NumLicensePackages"] = numLicensePackages
|
||||
ctx.Data["EnableLicenses"] = numLicensePackages > 0
|
||||
ctx.Data["IsRepoAdmin"] = ctx.Repo.Permission.IsAdmin()
|
||||
|
||||
ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name
|
||||
ctx.Data["PageTitleCommon"] = repo.Name + " - " + setting.AppName
|
||||
ctx.Data["Repository"] = repo
|
||||
|
||||
@@ -110,12 +110,14 @@ type RepoSettingForm struct {
|
||||
EnablePrune bool
|
||||
|
||||
// Advanced settings
|
||||
EnableCode bool
|
||||
EnableCode bool
|
||||
CodeVisibility string
|
||||
|
||||
EnableWiki bool
|
||||
EnableExternalWiki bool
|
||||
DefaultWikiBranch string
|
||||
ExternalWikiURL string
|
||||
EnableWiki bool
|
||||
EnableExternalWiki bool
|
||||
DefaultWikiBranch string
|
||||
ExternalWikiURL string
|
||||
WikiVisibility string
|
||||
|
||||
EnableIssues bool
|
||||
EnableExternalTracker bool
|
||||
@@ -124,13 +126,15 @@ type RepoSettingForm struct {
|
||||
TrackerIssueStyle string
|
||||
ExternalTrackerRegexpPattern string
|
||||
EnableCloseIssuesViaCommitInAnyBranch bool
|
||||
IssuesVisibility string
|
||||
|
||||
EnableProjects bool
|
||||
ProjectsMode string
|
||||
|
||||
EnableReleases bool
|
||||
EnableReleases bool
|
||||
ReleasesVisibility string
|
||||
|
||||
EnablePackages bool
|
||||
EnablePackages bool
|
||||
|
||||
EnablePulls bool
|
||||
PullsIgnoreWhitespace bool
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
@@ -56,20 +57,24 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository) (*Do
|
||||
Module: repo.Name,
|
||||
}
|
||||
|
||||
// Resolve effective streams.
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Track best release per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
}
|
||||
}
|
||||
|
||||
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
rel, ok := bestByChannel[ch]
|
||||
if !ok {
|
||||
continue
|
||||
@@ -91,7 +96,10 @@ func GenerateDolibarrJSON(ctx context.Context, repo *repo_model.Repository) (*Do
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := channelSuffix(ch)
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch)
|
||||
}
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/licenses"
|
||||
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
)
|
||||
@@ -64,22 +65,54 @@ type xmlTargetPlat struct {
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
// Joomla update stream names (full convention).
|
||||
const (
|
||||
ChannelStable = "stable"
|
||||
ChannelReleaseCandidate = "release-candidate"
|
||||
ChannelBeta = "beta"
|
||||
ChannelAlpha = "alpha"
|
||||
ChannelDevelopment = "development"
|
||||
)
|
||||
|
||||
// AllChannels in display order (most stable first).
|
||||
var AllChannels = []string{ChannelStable, ChannelReleaseCandidate, ChannelBeta, ChannelAlpha, ChannelDevelopment}
|
||||
|
||||
// channelFromTag maps a release tag name to a Joomla update channel.
|
||||
func channelFromTag(tagName string, isPrerelease bool) string {
|
||||
lower := strings.ToLower(tagName)
|
||||
switch {
|
||||
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
|
||||
return "dev"
|
||||
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
|
||||
return "alpha"
|
||||
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
|
||||
return "beta"
|
||||
return ChannelDevelopment
|
||||
case strings.Contains(lower, "-alpha"):
|
||||
return ChannelAlpha
|
||||
case strings.Contains(lower, "-beta"):
|
||||
return ChannelBeta
|
||||
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
|
||||
return "rc"
|
||||
return ChannelReleaseCandidate
|
||||
case isPrerelease:
|
||||
return "rc"
|
||||
return ChannelReleaseCandidate
|
||||
default:
|
||||
return "stable"
|
||||
return ChannelStable
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeChannel maps shorthand channel names to the full Joomla convention.
|
||||
// Accepts both "rc" and "release-candidate", "dev" and "development", etc.
|
||||
func NormalizeChannel(ch string) string {
|
||||
switch strings.ToLower(ch) {
|
||||
case "rc", "release-candidate":
|
||||
return ChannelReleaseCandidate
|
||||
case "dev", "development":
|
||||
return ChannelDevelopment
|
||||
case "alpha":
|
||||
return ChannelAlpha
|
||||
case "beta":
|
||||
return ChannelBeta
|
||||
case "stable":
|
||||
return ChannelStable
|
||||
default:
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,13 +143,16 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, allowed
|
||||
|
||||
element := strings.ToLower(repo.Name)
|
||||
|
||||
// Resolve effective streams (repo override → org default → Joomla default).
|
||||
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
|
||||
|
||||
// Track best (latest) release per channel to emit one entry per channel.
|
||||
bestByChannel := make(map[string]*repo_model.Release)
|
||||
for _, rel := range releases {
|
||||
if rel.IsDraft || rel.IsTag {
|
||||
continue
|
||||
}
|
||||
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
|
||||
ch := licenses.MatchStreamFromTag(rel.TagName, rel.IsPrerelease, streams)
|
||||
existing, ok := bestByChannel[ch]
|
||||
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
||||
bestByChannel[ch] = rel
|
||||
@@ -124,15 +160,17 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, allowed
|
||||
}
|
||||
|
||||
// Build allowed channel set for filtering.
|
||||
// Normalize shorthand names so both "rc" and "release-candidate" work.
|
||||
channelAllowed := make(map[string]bool)
|
||||
if len(allowedChannels) > 0 {
|
||||
for _, c := range allowedChannels {
|
||||
channelAllowed[strings.ToLower(c)] = true
|
||||
channelAllowed[NormalizeChannel(c)] = true
|
||||
}
|
||||
}
|
||||
|
||||
var updates xmlUpdates
|
||||
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
|
||||
for _, stream := range streams {
|
||||
ch := stream.Name
|
||||
// Skip channels not in the allowed set (when filtering is active).
|
||||
if len(channelAllowed) > 0 && !channelAllowed[ch] {
|
||||
continue
|
||||
@@ -161,7 +199,10 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, allowed
|
||||
}
|
||||
|
||||
version := extractVersion(rel.TagName)
|
||||
suffix := channelSuffix(ch)
|
||||
suffix := stream.Suffix
|
||||
if suffix == "" {
|
||||
suffix = channelSuffix(ch) // fallback for Joomla defaults
|
||||
}
|
||||
if suffix != "" {
|
||||
version = version + suffix
|
||||
}
|
||||
@@ -223,13 +264,13 @@ func extractVersion(tagName string) string {
|
||||
// channelSuffix returns the version suffix for a channel.
|
||||
func channelSuffix(channel string) string {
|
||||
switch channel {
|
||||
case "dev":
|
||||
case ChannelDevelopment:
|
||||
return "-dev"
|
||||
case "alpha":
|
||||
case ChannelAlpha:
|
||||
return "-alpha"
|
||||
case "beta":
|
||||
case ChannelBeta:
|
||||
return "-beta"
|
||||
case "rc":
|
||||
case ChannelReleaseCandidate:
|
||||
return "-rc"
|
||||
default:
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
|
||||
{{if .NewMasterKey}}
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NewKeyCreated}}
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<h4 class="ui top attached header tw-flex tw-justify-between tw-items-center">
|
||||
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
|
||||
{{if .IsRepoAdmin}}
|
||||
<a class="ui small primary button" href="#new-package-form" onclick="this.parentElement.parentElement.nextElementSibling.querySelector('#new-package-form').style.display='block'; return false;">
|
||||
{{ctx.Locale.Tr "repo.licenses.new_package"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .IsRepoAdmin}}
|
||||
<div id="new-package-form" style="display: none;" class="tw-mb-4">
|
||||
<form class="ui form" method="post" action="{{$.Org.HomeLink}}/-/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
|
||||
<input name="name" required placeholder="Pro Annual">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
|
||||
<input name="description" placeholder="Annual pro subscription">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
|
||||
<input name="duration_days" type="number" value="0" min="0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="0" min="0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
<input name="allowed_channels" placeholder="stable,release-candidate">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.duration"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.channels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.keys_issued"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/generate" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="package_id" value="{{.ID}}">
|
||||
<button class="ui tiny primary button" type="submit">
|
||||
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-key" 48}}
|
||||
<h2>{{ctx.Locale.Tr "repo.licenses.none"}}</h2>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.none_desc"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .LicenseKeys}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.licensee"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.expires"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code>{{if .IsInternal}} <span class="ui tiny orange label">Master</span>{{end}}</td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.TimeSince .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.Org.HomeLink}}/-/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -25,6 +25,11 @@
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .IsOrganizationMember}}
|
||||
<a class="{{if .IsLicensesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/licenses">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and .IsRepoIndexerEnabled .CanReadCode}}
|
||||
<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
|
||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
|
||||
|
||||
@@ -128,6 +128,15 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if or .EnableLicenses .IsRepoAdmin}}
|
||||
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
|
||||
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
|
||||
{{if .NumLicensePackages}}
|
||||
<span class="ui small label">{{CountFmt .NumLicensePackages}}</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
|
||||
{{if and (not ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
|
||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
|
||||
{{if .NewMasterKey}}
|
||||
<div class="ui info message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.master_key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.master_key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewMasterKey}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NewKeyCreated}}
|
||||
<div class="ui success message">
|
||||
<div class="header">{{ctx.Locale.Tr "repo.licenses.key_created"}}</div>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.key_created_copy"}}</p>
|
||||
<code class="tw-text-lg tw-select-all">{{.NewKeyCreated}}</code>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── License Packages ── */}}
|
||||
<h4 class="ui top attached header tw-flex tw-justify-between tw-items-center">
|
||||
<span>{{svg "octicon-key" 16}} {{ctx.Locale.Tr "repo.licenses.packages"}}</span>
|
||||
{{if .IsRepoAdmin}}
|
||||
<a class="ui small primary button" href="#new-package-form" onclick="document.getElementById('new-package-form').style.display=document.getElementById('new-package-form').style.display==='none'?'block':'none'; return false;">
|
||||
{{ctx.Locale.Tr "repo.licenses.new_package"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .IsRepoAdmin}}
|
||||
<div id="new-package-form" style="display: none;" class="tw-mb-4">
|
||||
<form class="ui form" method="post" action="{{.RepoLink}}/licenses/packages">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="two fields">
|
||||
<div class="required field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.package_name"}}</label>
|
||||
<input name="name" required placeholder="Pro Annual">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
|
||||
<input name="description" placeholder="Annual pro subscription">
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.duration"}} ({{ctx.Locale.Tr "repo.licenses.days"}})</label>
|
||||
<input name="duration_days" type="number" value="0" min="0" placeholder="0 = lifetime">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.max_sites"}}</label>
|
||||
<input name="max_sites" type="number" value="0" min="0" placeholder="0 = unlimited">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.licenses.channels"}}</label>
|
||||
<input name="allowed_channels" placeholder='stable,release-candidate'>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.licenses.channels_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.licenses.create_package"}}</button>
|
||||
</form>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .LicensePackages}}
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.package_name"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.duration"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.channels"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.keys_issued"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicensePackages}}
|
||||
<tr>
|
||||
<td><strong>{{.Name}}</strong>{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}</td>
|
||||
<td>{{if eq .DurationDays 0}}{{ctx.Locale.Tr "repo.licenses.lifetime"}}{{else}}{{.DurationDays}} {{ctx.Locale.Tr "repo.licenses.days"}}{{end}}</td>
|
||||
<td>{{if .AllowedChannels}}<code>{{.AllowedChannels}}</code>{{else}}{{ctx.Locale.Tr "repo.licenses.all_channels"}}{{end}}</td>
|
||||
<td>{{.KeyCount}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/keys/generate" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="package_id" value="{{.ID}}">
|
||||
<button class="ui tiny primary button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.generate_key"}}">
|
||||
{{svg "octicon-plus" 14}} {{ctx.Locale.Tr "repo.licenses.generate_key"}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-key" 48}}
|
||||
<h2>{{ctx.Locale.Tr "repo.licenses.none"}}</h2>
|
||||
<p>{{ctx.Locale.Tr "repo.licenses.none_desc"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* ── Issued Keys ── */}}
|
||||
{{if .LicenseKeys}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.licenses.issued_keys"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.key_prefix"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.licensee"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.expires"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.licenses.status"}}</th>
|
||||
{{if .IsRepoAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LicenseKeys}}
|
||||
<tr>
|
||||
<td><code>{{.KeyPrefix}}</code></td>
|
||||
<td>{{.LicenseeName}}{{if .LicenseeEmail}} <small>({{.LicenseeEmail}})</small>{{end}}</td>
|
||||
<td>{{if eq .ExpiresUnix 0}}{{ctx.Locale.Tr "repo.licenses.never"}}{{else}}{{DateUtils.TimeSince .ExpiresUnix}}{{end}}</td>
|
||||
<td>{{if .IsActive}}<span class="ui green label">{{ctx.Locale.Tr "repo.licenses.active"}}</span>{{else}}<span class="ui grey label">{{ctx.Locale.Tr "repo.licenses.inactive"}}</span>{{end}}</td>
|
||||
{{if $.IsRepoAdmin}}
|
||||
<td class="tw-text-right">
|
||||
<form method="post" action="{{$.RepoLink}}/licenses/keys/{{.ID}}/revoke" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "repo.licenses.revoke"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ── Update Feed Info ── */}}
|
||||
<h4 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "repo.licenses.update_feeds"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="field">
|
||||
<label>Joomla updates.xml</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates.xml" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-mt-2">
|
||||
<label>Dolibarr JSON</label>
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="text" readonly value="{{.Repository.HTMLURL ctx}}/updates/dolibarr.json" onclick="this.select()">
|
||||
<button class="ui button" onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">{{svg "octicon-copy" 14}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
@@ -16,6 +16,11 @@
|
||||
{{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if not .PageIsTagList}}
|
||||
<a class="ui small button" href="{{.RepoLink}}/updates.xml" target="_blank">
|
||||
{{svg "octicon-download" 16}} {{ctx.Locale.Tr "repo.release.update_feed"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and (not .PageIsTagList) .CanCreateRelease}}
|
||||
<a class="ui small primary button" href="{{$.RepoLink}}/releases/new{{if .PageIsSingleTag}}?tag={{.SingleReleaseTagName}}{{end}}">
|
||||
{{ctx.Locale.Tr "repo.release.new_release"}}
|
||||
|
||||
@@ -330,6 +330,13 @@
|
||||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
|
||||
<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="wiki_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
@@ -389,6 +396,13 @@
|
||||
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{if .Repository.CloseIssuesViaCommitInAnyBranch}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>
|
||||
</div>
|
||||
<div class="inline field tw-mt-2">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="issues_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox{{if $isExternalTrackerGlobalDisabled}} disabled{{end}}"{{if $isExternalTrackerGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
@@ -487,10 +501,20 @@
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.releases"}}</label>
|
||||
<div class="ui checkbox{{if $isReleasesGlobalDisabled}} disabled{{end}}"{{if $isReleasesGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||
<input class="enable-system" name="enable_releases" type="checkbox" {{if $isReleasesEnabled}}checked{{end}}>
|
||||
<input class="enable-system" name="enable_releases" type="checkbox" data-target="#releases_visibility_box" {{if $isReleasesEnabled}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.releases_desc"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field tw-pl-4{{if not $isReleasesEnabled}} disabled{{end}}" id="releases_visibility_box">
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.unit_visibility"}}</label>
|
||||
<select name="releases_visibility" class="ui dropdown">
|
||||
<option value="not-set" {{if not (eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1)}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_private"}}</option>
|
||||
<option value="anonymous-read" {{if eq (.Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeReleases).AnonymousAccessMode 1}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.unit_visibility_public"}}</option>
|
||||
</select>
|
||||
<p class="help">{{ctx.Locale.Tr "repo.settings.unit_visibility_releases_help"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$isPackagesEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypePackages}}
|
||||
{{$isPackagesGlobalDisabled := ctx.Consts.RepoUnitTypePackages.UnitGlobalDisabled}}
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 05.07.00
|
||||
VERSION: 05.13.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -87,13 +87,13 @@
|
||||
<element>mokogitea</element>
|
||||
<type>application</type>
|
||||
<client>site</client>
|
||||
<version>05.05.00</version>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<version>05.13.00</version>
|
||||
<creationDate>2026-05-31</creationDate>
|
||||
<infourl title='MokoGitea'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.05.00.zip</downloadurl>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/releases/download/stable/mokogitea-05.13.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>4fee9eb03e4b819a63bce2ceb54fdce0d3eb8bf5b31460fcc42e5ecd75cc856e</sha256>
|
||||
<sha256>b8d32fb99a0fde25ea0860b55ac5f3dfb8a15576863d9ddb80e67c5895722dcd</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user