2
0

feat(i18n): add organization license settings and AI template helpers
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Failing after 1m46s
Build and Release / Unit Tests (push) Successful in 3m14s
Build and Release / Lint (push) Failing after 3m39s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped

Add organization-level license management with new settings page and locale strings across all languages. Add AI feature detection helpers (IsAIEnabled, IsAICodeReviewEnabled, IsAIIssueTriageEnabled) to template functions. Add license scanning functionality to repository settings.
This commit is contained in:
2026-01-19 17:19:32 -05:00
parent a91f76bbab
commit d352b138d7
41 changed files with 804 additions and 76 deletions

View File

@@ -12,6 +12,7 @@ import (
"strings"
"time"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/base"
"code.gitcaddy.com/server/v3/modules/htmlutil"
"code.gitcaddy.com/server/v3/modules/markup"
@@ -165,9 +166,23 @@ func NewFuncMap() template.FuncMap {
"FilenameIsImage": filenameIsImage,
"TabSizeClass": tabSizeClass,
// -----------------------------------------------------------------
// AI features
"IsAIEnabled": ai.IsEnabled,
"IsAICodeReviewEnabled": isAICodeReviewEnabled,
"IsAIIssueTriageEnabled": isAIIssueTriageEnabled,
}
}
func isAICodeReviewEnabled() bool {
return ai.IsEnabled() && setting.AI.EnableCodeReview
}
func isAIIssueTriageEnabled() bool {
return ai.IsEnabled() && setting.AI.EnableIssueTriage
}
// SanitizeHTML sanitizes the input by default sanitization rules.
func SanitizeHTML(s string) template.HTML {
return markup.Sanitize(s)

View File

@@ -4052,5 +4052,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4251,5 +4251,14 @@
"vault.max_secrets": "Max. Geheimnisse",
"vault.max_versions": "Max. Versionen",
"vault.audit_retention": "Audit-Aufbewahrung",
"vault.unlimited": "Unbegrenzt"
"vault.unlimited": "Unbegrenzt",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -3745,5 +3745,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2823,6 +2823,12 @@
"org.settings.confirm_delete_account": "Confirm Deletion",
"org.settings.delete_failed": "Deleting organization failed due to an internal error",
"org.settings.delete_successful": "Organization <b>%s</b> has been deleted successfully.",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"org.settings.homepage_pinning": "Homepage Visibility",
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
@@ -3917,6 +3923,9 @@
"repo.settings.license_file_error": "License saved but failed to create LICENSE.md file. You may need to create it manually.",
"repo.settings.current_license": "Current License",
"repo.settings.view_license_file": "View LICENSE.md file",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository.",
"api": "API",
"admin.config.api_header_url": "API Header Link",
"admin.config.api_header_url_placeholder": "https://example.com/api/docs",

View File

@@ -3843,5 +3843,14 @@
"vault.max_secrets": "Máx. secretos",
"vault.max_versions": "Máx. versiones",
"vault.audit_retention": "Retención de auditoría",
"vault.unlimited": "Ilimitado"
"vault.unlimited": "Ilimitado",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2976,5 +2976,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2243,5 +2243,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4189,5 +4189,14 @@
"vault.max_secrets": "Max. secrets",
"vault.max_versions": "Max. versions",
"vault.audit_retention": "Rétention d'audit",
"vault.unlimited": "Illimité"
"vault.unlimited": "Illimité",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4125,5 +4125,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4250,5 +4250,14 @@
"vault.max_secrets": "Max Secrets",
"vault.max_versions": "Max Versions",
"vault.audit_retention": "Audit Retention",
"vault.unlimited": "Unlimited"
"vault.unlimited": "Unlimited",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2160,5 +2160,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -1984,5 +1984,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -1872,5 +1872,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -3289,5 +3289,14 @@
"vault.max_secrets": "Max. segreti",
"vault.max_versions": "Max. versioni",
"vault.audit_retention": "Conservazione audit",
"vault.unlimited": "Illimitato"
"vault.unlimited": "Illimitato",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4232,5 +4232,14 @@
"vault.max_secrets": "最大シークレット数",
"vault.max_versions": "最大バージョン数",
"vault.audit_retention": "監査保持期間",
"vault.unlimited": "無制限"
"vault.unlimited": "無制限",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2247,5 +2247,14 @@
"vault.max_secrets": "최대 시크릿",
"vault.max_versions": "최대 버전",
"vault.audit_retention": "감사 보존",
"vault.unlimited": "무제한"
"vault.unlimited": "무제한",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -3766,5 +3766,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2914,5 +2914,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2885,5 +2885,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4030,5 +4030,14 @@
"vault.max_secrets": "Máx. segredos",
"vault.max_versions": "Máx. versões",
"vault.audit_retention": "Retenção de auditoria",
"vault.unlimited": "Ilimitado"
"vault.unlimited": "Ilimitado",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4126,5 +4126,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -3839,5 +3839,14 @@
"vault.max_secrets": "Макс. секретов",
"vault.max_versions": "Макс. версий",
"vault.audit_retention": "Хранение аудита",
"vault.unlimited": "Неограниченно"
"vault.unlimited": "Неограниченно",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2931,5 +2931,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -1956,5 +1956,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -2515,5 +2515,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4124,5 +4124,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -3896,5 +3896,14 @@
"admin.plugins.license_grace": "Grace Period",
"admin.plugins.license_invalid": "Invalid",
"admin.plugins.license_not_required": "Free",
"admin.plugins.none": "No plugins loaded"
"admin.plugins.none": "No plugins loaded",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4251,5 +4251,14 @@
"vault.max_secrets": "最大密钥数",
"vault.max_versions": "最大版本数",
"vault.audit_retention": "审计保留",
"vault.unlimited": "无限制"
"vault.unlimited": "无限制",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -4159,5 +4159,14 @@
"vault.max_secrets": "最大密鑰數",
"vault.max_versions": "最大版本數",
"vault.audit_retention": "稽核保留",
"vault.unlimited": "無限制"
"vault.unlimited": "無限制",
"org.settings.license": "License",
"org.settings.license_type": "Organization License",
"org.settings.license_help": "Set a license for your organization. This will be stored in your .profile repository.",
"org.settings.license_saved": "License updated successfully.",
"org.settings.license_cleared": "License has been cleared.",
"org.settings.license_error": "Failed to update license.",
"repo.settings.license_scan": "Scan",
"repo.settings.license_detected": "License detected",
"repo.settings.license_not_found": "No license file detected in repository."
}

View File

@@ -8,9 +8,10 @@ import (
issues_model "code.gitcaddy.com/server/v3/models/issues"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/json"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/services/context"
ai_service "code.gitcaddy.com/server/v3/services/ai"
"code.gitcaddy.com/server/v3/services/context"
)
// AIReviewPullRequest performs an AI-powered code review on a pull request
@@ -269,7 +270,7 @@ func AIExplainCode(ctx *context.APIContext) {
}
var req ExplainCodeRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
@@ -291,9 +292,9 @@ func AIExplainCode(ctx *context.APIContext) {
type GenerateDocRequest struct {
Code string `json:"code" binding:"Required"`
FilePath string `json:"file_path"`
DocType string `json:"doc_type"` // function, class, module, api
DocType string `json:"doc_type"` // function, class, module, api
Language string `json:"language"`
Style string `json:"style"` // jsdoc, docstring, xml, markdown
Style string `json:"style"` // jsdoc, docstring, xml, markdown
}
// AIGenerateDocumentation generates documentation using AI
@@ -343,7 +344,7 @@ func AIGenerateDocumentation(ctx *context.APIContext) {
}
var req GenerateDocRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
@@ -384,12 +385,12 @@ func AIStatus(ctx *context.APIContext) {
// "$ref": "#/responses/AIStatusResponse"
status := map[string]any{
"enabled": ai_service.IsEnabled(),
"code_review_enabled": setting.AI.EnableCodeReview,
"enabled": ai_service.IsEnabled(),
"code_review_enabled": setting.AI.EnableCodeReview,
"issue_triage_enabled": setting.AI.EnableIssueTriage,
"doc_gen_enabled": setting.AI.EnableDocGen,
"doc_gen_enabled": setting.AI.EnableDocGen,
"explain_code_enabled": setting.AI.EnableExplainCode,
"chat_enabled": setting.AI.EnableChat,
"chat_enabled": setting.AI.EnableChat,
}
if ai_service.IsEnabled() {

View File

@@ -14,6 +14,7 @@ import (
"code.gitcaddy.com/server/v3/models/renderhelper"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/git"
"code.gitcaddy.com/server/v3/modules/gitrepo"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/markup/markdown"
"code.gitcaddy.com/server/v3/modules/setting"
@@ -224,6 +225,14 @@ func home(ctx *context.Context, viewRepositories bool) {
}
ctx.Data["OrgStats"] = orgStats
// Load organization license from .profile repo
profileRepo, orgLicense := getOrgProfileLicense(ctx, org)
if orgLicense != "" {
ctx.Data["OrgLicense"] = orgLicense
ctx.Data["ProfileRepoLink"] = profileRepo.Link()
ctx.Data["ProfileRepo"] = profileRepo
}
// Always show overview by default for organizations
isViewOverview := !viewRepositories
// Load profile readme if available
@@ -385,3 +394,82 @@ func CreateProfileRepo(ctx *context.Context) {
// Redirect to edit the README
ctx.Redirect(repo.Link() + "/_edit/main/README.md")
}
// getOrgProfileLicense gets the license from the org's .profile repo
func getOrgProfileLicense(ctx *context.Context, org *organization.Organization) (*repo_model.Repository, string) {
profileRepo, err := repo_model.GetRepositoryByName(ctx, org.ID, ".profile")
if err != nil {
return nil, ""
}
if profileRepo.IsEmpty {
return profileRepo, ""
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileRepo)
if err != nil {
log.Error("getOrgProfileLicense: failed to open git repo: %v", err)
return profileRepo, ""
}
commit, err := gitRepo.GetBranchCommit(profileRepo.DefaultBranch)
if err != nil {
log.Error("getOrgProfileLicense: failed to get branch commit: %v", err)
return profileRepo, ""
}
// Check for LICENSE files
licenseFiles := []string{"LICENSE.md", "LICENSE", "LICENSE.txt", "COPYING"}
for _, filename := range licenseFiles {
entry, err := commit.GetTreeEntryByPath(filename)
if err == nil && !entry.IsDir() {
content, err := entry.Blob().GetBlobContent(10000)
if err == nil {
license := detectOrgLicenseType(content)
if license != "" {
return profileRepo, license
}
}
}
}
return profileRepo, ""
}
// detectOrgLicenseType tries to detect the license type from content
func detectOrgLicenseType(content string) string {
content = strings.ToLower(content)
// Check for common license signatures
licensePatterns := map[string][]string{
"MIT": {"mit license", "permission is hereby granted, free of charge"},
"Apache-2.0": {"apache license", "version 2.0"},
"GPL-3.0": {"gnu general public license", "version 3"},
"GPL-2.0": {"gnu general public license", "version 2"},
"BSD-3-Clause": {"redistribution and use in source and binary forms", "neither the name"},
"BSD-2-Clause": {"redistribution and use in source and binary forms"},
"LGPL-3.0": {"gnu lesser general public license", "version 3"},
"LGPL-2.1": {"gnu lesser general public license", "version 2.1"},
"MPL-2.0": {"mozilla public license", "version 2.0"},
"AGPL-3.0": {"gnu affero general public license", "version 3"},
"BSL-1.1": {"business source license", "change date"},
"BSL-1.0": {"boost software license"},
"Unlicense": {"this is free and unencumbered software", "unlicense"},
"CC0-1.0": {"cc0 1.0 universal", "public domain"},
"SSPL-1.0": {"server side public license"},
}
for license, patterns := range licensePatterns {
matchCount := 0
for _, pattern := range patterns {
if strings.Contains(content, pattern) {
matchCount++
}
}
if matchCount == len(patterns) {
return license
}
}
return ""
}

View File

@@ -0,0 +1,160 @@
// Copyright 2024 The Gitea Authors and MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"bytes"
"fmt"
"net/http"
"time"
"code.gitcaddy.com/server/v3/models/organization"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/log"
repo_module "code.gitcaddy.com/server/v3/modules/repository"
"code.gitcaddy.com/server/v3/modules/templates"
repo_setting "code.gitcaddy.com/server/v3/routers/web/repo/setting"
shared_user "code.gitcaddy.com/server/v3/routers/web/shared/user"
"code.gitcaddy.com/server/v3/services/context"
repo_service "code.gitcaddy.com/server/v3/services/repository"
files_service "code.gitcaddy.com/server/v3/services/repository/files"
)
const tplSettingsLicense templates.TplName = "org/settings/license"
// SettingsLicense shows the organization license settings page
func SettingsLicense(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.license")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsLicense"] = true
ctx.Data["LicenseTypes"] = repo_setting.LicenseTypes
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return
}
// Load current license from .profile repo
profileRepo, license := getOrgProfileLicense(ctx, ctx.Org.Organization)
if license != "" {
ctx.Data["CurrentLicense"] = license
ctx.Data["ProfileRepo"] = profileRepo
}
ctx.HTML(http.StatusOK, tplSettingsLicense)
}
// SettingsLicensePost handles license settings form submission
func SettingsLicensePost(ctx *context.Context) {
licenseType := ctx.FormString("license_type")
org := ctx.Org.Organization
ctx.Data["Title"] = ctx.Tr("org.settings.license")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsLicense"] = true
ctx.Data["LicenseTypes"] = repo_setting.LicenseTypes
// Get or create .profile repo
profileRepo, err := getOrCreateProfileRepo(ctx, org)
if err != nil {
ctx.ServerError("getOrCreateProfileRepo", err)
return
}
if licenseType == "" {
// Clear license - delete LICENSE.md if it exists
ctx.Flash.Success(ctx.Tr("org.settings.license_cleared"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/license")
return
}
// Create LICENSE.md in .profile repo
if err := createOrgLicenseFile(ctx, profileRepo, licenseType); err != nil {
log.Error("Failed to create LICENSE.md: %v", err)
ctx.Flash.Error(ctx.Tr("org.settings.license_error"))
} else {
ctx.Flash.Success(ctx.Tr("org.settings.license_saved"))
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/license")
}
// SettingsLicenseScan detects existing license in .profile repo
func SettingsLicenseScan(ctx *context.Context) {
profileRepo, license := getOrgProfileLicense(ctx, ctx.Org.Organization)
if license != "" {
ctx.JSON(http.StatusOK, map[string]any{
"found": true,
"license": license,
"file": "LICENSE.md",
"repo": profileRepo.Name,
})
return
}
ctx.JSON(http.StatusOK, map[string]any{"found": false})
}
// getOrCreateProfileRepo gets or creates the .profile repo for an org
func getOrCreateProfileRepo(ctx *context.Context, org *organization.Organization) (*repo_model.Repository, error) {
profileRepo, err := repo_model.GetRepositoryByName(ctx, org.ID, ".profile")
if err == nil {
return profileRepo, nil
}
if !repo_model.IsErrRepoNotExist(err) {
return nil, err
}
// Create .profile repo
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 {
return nil, err
}
return repo, nil
}
// createOrgLicenseFile creates a LICENSE.md file in the profile repo
func createOrgLicenseFile(ctx *context.Context, repo *repo_model.Repository, licenseType string) error {
// Get license content from templates
licenseContent, err := repo_module.GetLicense(licenseType, &repo_module.LicenseValues{
Owner: repo.OwnerName,
Email: ctx.Doer.Email,
Repo: repo.Name,
Year: time.Now().Format("2006"),
})
if err != nil {
return fmt.Errorf("GetLicense: %w", err)
}
// Create/update LICENSE.md using files service
opts := &files_service.ChangeRepoFilesOptions{
Message: fmt.Sprintf("Add LICENSE.md (%s)", licenseType),
OldBranch: repo.DefaultBranch,
NewBranch: repo.DefaultBranch,
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "LICENSE.md",
ContentReader: bytes.NewReader(licenseContent),
},
},
}
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
if err != nil {
// If file already exists, try to update it instead
opts.Files[0].Operation = "update"
_, err = files_service.ChangeRepoFiles(ctx, repo, ctx.Doer, opts)
}
return err
}

View File

@@ -8,9 +8,8 @@ import (
issues_model "code.gitcaddy.com/server/v3/models/issues"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/modules/templates"
"code.gitcaddy.com/server/v3/services/context"
ai_service "code.gitcaddy.com/server/v3/services/ai"
"code.gitcaddy.com/server/v3/services/context"
)
// AIReviewPullRequest handles the AI review request for a pull request
@@ -34,7 +33,7 @@ func AIReviewPullRequest(ctx *context.Context) {
}
if !issue.IsPull {
ctx.NotFound("Not a pull request", nil)
ctx.NotFound(nil)
return
}
@@ -96,7 +95,7 @@ func AITriageIssue(ctx *context.Context) {
func AISuggestLabels(ctx *context.Context) {
if !ai_service.IsEnabled() {
ctx.JSON(http.StatusServiceUnavailable, map[string]string{
"error": ctx.Tr("repo.ai.service_unavailable"),
"error": "AI service is not available",
})
return
}
@@ -119,25 +118,3 @@ func AISuggestLabels(ctx *context.Context) {
ctx.JSON(http.StatusOK, suggestions)
}
// IsAIEnabled is a template helper to check if AI is enabled
func IsAIEnabled() bool {
return ai_service.IsEnabled()
}
// IsAICodeReviewEnabled checks if AI code review is enabled
func IsAICodeReviewEnabled() bool {
return ai_service.IsEnabled() && setting.AI.EnableCodeReview
}
// IsAIIssueTriageEnabled checks if AI issue triage is enabled
func IsAIIssueTriageEnabled() bool {
return ai_service.IsEnabled() && setting.AI.EnableIssueTriage
}
func init() {
// Register template functions
templates.RegisterTemplateFunc("IsAIEnabled", IsAIEnabled)
templates.RegisterTemplateFunc("IsAICodeReviewEnabled", IsAICodeReviewEnabled)
templates.RegisterTemplateFunc("IsAIIssueTriageEnabled", IsAIIssueTriageEnabled)
}

View File

@@ -7,9 +7,11 @@ import (
"bytes"
"fmt"
"net/http"
"strings"
"time"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/gitrepo"
"code.gitcaddy.com/server/v3/modules/log"
repo_module "code.gitcaddy.com/server/v3/modules/repository"
"code.gitcaddy.com/server/v3/modules/templates"
@@ -67,7 +69,8 @@ var LicenseTypes = []LicenseCategory{
{
Category: "Source-Available",
Licenses: []LicenseInfo{
{"BSL-1.0", "Business Source License", "Free to use, becomes open source later"},
{"BSL-1.1", "Business Source License 1.1", "Commercial use restricted until change date"},
{"BSL-1.0", "Boost Software License", "Permissive, similar to MIT"},
{"SSPL-1.0", "Server Side Public License", "Requires publishing entire service stack"},
},
},
@@ -153,3 +156,87 @@ func createLicenseFile(ctx *context.Context, repo *repo_model.Repository, licens
}
return err
}
// LicenseScan detects existing license files in the repository
func LicenseScan(ctx *context.Context) {
repo := ctx.Repo.Repository
if repo.IsEmpty {
ctx.JSON(http.StatusOK, map[string]any{"found": false})
return
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
log.Error("LicenseScan: failed to open git repo: %v", err)
ctx.JSON(http.StatusOK, map[string]any{"found": false})
return
}
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
log.Error("LicenseScan: failed to get branch commit: %v", err)
ctx.JSON(http.StatusOK, map[string]any{"found": false})
return
}
// Check for LICENSE files
licenseFiles := []string{"LICENSE.md", "LICENSE", "LICENSE.txt", "COPYING"}
for _, filename := range licenseFiles {
entry, err := commit.GetTreeEntryByPath(filename)
if err == nil && !entry.IsDir() {
content, err := entry.Blob().GetBlobContent(10000)
if err == nil {
license := detectLicenseType(content)
if license != "" {
ctx.JSON(http.StatusOK, map[string]any{
"found": true,
"license": license,
"file": filename,
})
return
}
}
}
}
ctx.JSON(http.StatusOK, map[string]any{"found": false})
}
// detectLicenseType tries to detect the license type from content
func detectLicenseType(content string) string {
content = strings.ToLower(content)
// Check for common license signatures
licensePatterns := map[string][]string{
"MIT": {"mit license", "permission is hereby granted, free of charge"},
"Apache-2.0": {"apache license", "version 2.0"},
"GPL-3.0": {"gnu general public license", "version 3"},
"GPL-2.0": {"gnu general public license", "version 2"},
"BSD-3-Clause": {"redistribution and use in source and binary forms", "neither the name"},
"BSD-2-Clause": {"redistribution and use in source and binary forms"},
"LGPL-3.0": {"gnu lesser general public license", "version 3"},
"LGPL-2.1": {"gnu lesser general public license", "version 2.1"},
"MPL-2.0": {"mozilla public license", "version 2.0"},
"AGPL-3.0": {"gnu affero general public license", "version 3"},
"BSL-1.1": {"business source license", "change date"},
"BSL-1.0": {"boost software license"},
"Unlicense": {"this is free and unencumbered software", "unlicense"},
"CC0-1.0": {"cc0 1.0 universal", "public domain"},
"SSPL-1.0": {"server side public license"},
}
for license, patterns := range licensePatterns {
matchCount := 0
for _, pattern := range patterns {
if strings.Contains(content, pattern) {
matchCount++
}
}
if matchCount == len(patterns) {
return license
}
}
return ""
}

View File

@@ -1031,6 +1031,12 @@ func registerWebRoutes(m *web.Router) {
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels)
})
m.Group("/license", func() {
m.Get("", org.SettingsLicense)
m.Post("", org.SettingsLicensePost)
m.Get("/scan", org.SettingsLicenseScan)
})
m.Group("/actions", func() {
m.Get("", org_setting.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
@@ -1158,6 +1164,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/license", func() {
m.Get("", repo_setting.License)
m.Post("", repo_setting.LicensePost)
m.Get("/scan", repo_setting.LicenseScan)
})
m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)

View File

@@ -8,10 +8,12 @@ import (
"fmt"
"strings"
"code.gitcaddy.com/server/v3/models/db"
issues_model "code.gitcaddy.com/server/v3/models/issues"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/modules/ai"
"code.gitcaddy.com/server/v3/modules/git"
"code.gitcaddy.com/server/v3/modules/gitrepo"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/services/gitdiff"
@@ -38,8 +40,15 @@ func ReviewPullRequest(ctx context.Context, pr *issues_model.PullRequest) (*ai.R
return nil, fmt.Errorf("failed to load issue: %w", err)
}
// Open git repo
gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
return nil, fmt.Errorf("failed to open git repo: %w", err)
}
defer gitRepo.Close()
// Get the diff
diff, err := gitdiff.GetDiff(ctx, pr.BaseRepo,
diff, err := gitdiff.GetDiffForAPI(ctx, gitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: pr.MergeBase,
AfterCommitID: pr.HeadCommitID,
@@ -115,7 +124,7 @@ func TriageIssue(ctx context.Context, issue *issues_model.Issue) (*ai.TriageIssu
}
// Get available labels
labels, err := issues_model.GetLabelsByRepoID(ctx, issue.RepoID, "", issues_model.ListOptions{})
labels, err := issues_model.GetLabelsByRepoID(ctx, issue.RepoID, "", db.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get labels: %w", err)
}
@@ -164,7 +173,7 @@ func SuggestLabels(ctx context.Context, issue *issues_model.Issue) (*ai.SuggestL
}
// Get available labels
labels, err := issues_model.GetLabelsByRepoID(ctx, issue.RepoID, "", issues_model.ListOptions{})
labels, err := issues_model.GetLabelsByRepoID(ctx, issue.RepoID, "", db.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get labels: %w", err)
}

View File

@@ -322,6 +322,21 @@
</div>
{{end}}
{{/* Organization License - Sidebar Card */}}
{{if .OrgLicense}}
<div class="ui segment tw-mt-4">
<div class="tw-flex tw-items-center tw-gap-3">
{{svg "octicon-law" 24}}
<div>
<div class="tw-font-semibold">{{ctx.Locale.Tr "repo.license"}}</div>
<a href="{{.ProfileRepoLink}}/src/{{.ProfileRepo.DefaultBranch}}/LICENSE.md" class="tw-text-lg">
{{.OrgLicense}}
</a>
</div>
</div>
</div>
{{end}}
{{/* Members/Public Members Section */}}
{{if .IsOrganizationMember}}
{{/* Internal view - show all members */}}

View File

@@ -0,0 +1,72 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings license")}}
<div class="ui segments org-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.license"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.license_type"}}</label>
<div class="tw-flex tw-gap-2">
<select name="license_type" id="license_type" class="ui dropdown tw-flex-1">
<option value="">{{ctx.Locale.Tr "repo.settings.license_none"}}</option>
{{range .LicenseTypes}}
<optgroup label="{{.Category}}">
{{range .Licenses}}
<option value="{{.Key}}" {{if eq $.CurrentLicense .Key}}selected{{end}} title="{{.Description}}">{{.Name}}</option>
{{end}}
</optgroup>
{{end}}
</select>
<button type="button" class="ui button" id="scan-license" data-url="{{.Link}}/scan">
{{svg "octicon-search" 16}} {{ctx.Locale.Tr "repo.settings.license_scan"}}
</button>
</div>
</div>
{{if .CurrentLicense}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.current_license"}}</label>
<p><strong>{{.CurrentLicense}}</strong></p>
{{if .ProfileRepo}}
<p class="help">
<a href="{{.ProfileRepo.Link}}/src/{{.ProfileRepo.DefaultBranch}}/LICENSE.md">{{ctx.Locale.Tr "repo.settings.view_license_file"}}</a>
</p>
{{end}}
</div>
{{end}}
<div class="help tw-mb-4">{{ctx.Locale.Tr "org.settings.license_help"}}</div>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</form>
</div>
</div>
<script>
document.getElementById('scan-license')?.addEventListener('click', async function() {
const btn = this;
const url = btn.dataset.url;
btn.classList.add('loading');
try {
const resp = await fetch(url);
const data = await resp.json();
if (data.found) {
document.getElementById('license_type').value = data.license;
// Show success message
const msg = document.createElement('div');
msg.className = 'ui positive message tw-mt-2';
msg.innerHTML = '<p>{{ctx.Locale.Tr "repo.settings.license_detected"}}: <strong>' + data.license + '</strong></p>';
btn.parentNode.appendChild(msg);
setTimeout(() => msg.remove(), 3000);
} else {
alert('{{ctx.Locale.Tr "repo.settings.license_not_found"}}');
}
} catch (e) {
console.error('License scan failed:', e);
} finally {
btn.classList.remove('loading');
}
});
</script>
{{template "org/settings/layout_footer" .}}

View File

@@ -12,6 +12,9 @@
<a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels">
{{ctx.Locale.Tr "repo.labels"}}
</a>
<a class="{{if .PageIsOrgSettingsLicense}}active {{end}}item" href="{{.OrgLink}}/settings/license">
{{ctx.Locale.Tr "org.settings.license"}}
</a>
{{if .EnableOAuth2}}
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications">
{{ctx.Locale.Tr "settings.applications"}}

View File

@@ -8,16 +8,21 @@
{{.CsrfTokenHtml}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.license_type"}}</label>
<select name="license_type" class="ui dropdown">
<option value="">{{ctx.Locale.Tr "repo.settings.license_none"}}</option>
{{range .LicenseTypes}}
<optgroup label="{{.Category}}">
{{range .Licenses}}
<option value="{{.Key}}" {{if eq $.Repository.LicenseType .Key}}selected{{end}} title="{{.Description}}">{{.Name}}</option>
<div class="tw-flex tw-gap-2">
<select name="license_type" id="license_type" class="ui dropdown tw-flex-1">
<option value="">{{ctx.Locale.Tr "repo.settings.license_none"}}</option>
{{range .LicenseTypes}}
<optgroup label="{{.Category}}">
{{range .Licenses}}
<option value="{{.Key}}" {{if eq $.Repository.LicenseType .Key}}selected{{end}} title="{{.Description}}">{{.Name}}</option>
{{end}}
</optgroup>
{{end}}
</optgroup>
{{end}}
</select>
</select>
<button type="button" class="ui button" id="scan-license" data-url="{{.Link}}/scan">
{{svg "octicon-search" 16}} {{ctx.Locale.Tr "repo.settings.license_scan"}}
</button>
</div>
</div>
{{if .Repository.LicenseType}}
<div class="field">
@@ -33,4 +38,32 @@
</form>
</div>
</div>
<script>
document.getElementById('scan-license')?.addEventListener('click', async function() {
const btn = this;
const url = btn.dataset.url;
btn.classList.add('loading');
try {
const resp = await fetch(url);
const data = await resp.json();
if (data.found) {
document.getElementById('license_type').value = data.license;
// Show success message
const msg = document.createElement('div');
msg.className = 'ui positive message tw-mt-2';
msg.innerHTML = '<p>{{ctx.Locale.Tr "repo.settings.license_detected"}}: <strong>' + data.license + '</strong></p>';
btn.parentNode.appendChild(msg);
setTimeout(() => msg.remove(), 3000);
} else {
alert('{{ctx.Locale.Tr "repo.settings.license_not_found"}}');
}
} catch (e) {
console.error('License scan failed:', e);
} finally {
btn.classList.remove('loading');
}
});
</script>
{{template "repo/settings/layout_footer" .}}