Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39150c6968 | |||
| 417bda1735 | |||
| f09aadf60c | |||
| fa54fe1ffc | |||
| 49fb7bb9a4 | |||
| 5dd98c04d8 | |||
| ea3957597c | |||
| 45e08616ac | |||
| 4dafcc5429 | |||
| 3159d53322 | |||
| fff64e6e7c | |||
| 1d4340a142 | |||
| 60670d066b | |||
| e8b2a485fc | |||
| cd496f159d | |||
| afbff02d81 | |||
| 8355b39ad4 | |||
| c2c9e053ff | |||
| 451b3022bd | |||
| 2153d7c916 | |||
| 63c801d595 | |||
| 0270be743f | |||
| dbf70a7def | |||
| 43b5a54ffa | |||
| b182855fc5 | |||
| ca6c8c958c | |||
| e9efbbc93b | |||
| 2482a3726e | |||
| 2f381dc16c | |||
| d2cdd9b1d6 | |||
| 6e0236d433 | |||
| ce83900967 | |||
| ee6405f4fd | |||
| 738117248d | |||
| d2d652e5b7 | |||
| 0166a6d02a | |||
| 0f543903fb | |||
| ec02fb9cf8 | |||
| f639940608 | |||
| 482ad13ff1 | |||
| 8b5abc6b1e | |||
| 7a06e44e24 | |||
| a804ebcf09 | |||
| 3ec28c7f6a | |||
| 6f49a9efbe | |||
| a2b951ea43 | |||
| 8b83c415cf | |||
| f36125926a | |||
| 0902e726ed | |||
| b4907ac75c | |||
| db4ab6c82e | |||
| 7b1f539f05 | |||
| 6c368f81a7 | |||
| 1ceecaa3de | |||
| 9b5ac9246b | |||
| 73f301102a | |||
| ee57494073 | |||
| 5fcdc7fff0 | |||
| b2d6ce9c34 | |||
| 9e78f1c367 | |||
| 0d702937fb | |||
| d891985aac | |||
| f372bcb998 | |||
| 8d942b2ebf | |||
| 01bb4a34ca | |||
| 3f97efb934 | |||
| a60f1b7fc8 | |||
| 026bbce088 | |||
| b6bfe109d9 | |||
| 69fe39374d | |||
| 6e3f9e2cdf | |||
| 22fa3d16bf | |||
| 6d2ccb76eb | |||
| a66f88e0bf | |||
| 58782a3920 | |||
| 53b7e378d1 | |||
| ad78bb7c27 | |||
| ff016ed888 | |||
| 33fd9c5620 | |||
| 6f1b83eb74 | |||
| d300cde639 | |||
| 05f1ac1a12 | |||
| 5d84da9ae8 | |||
| a8a01ed978 | |||
| b441b0a350 | |||
| fcbc28735e | |||
| 5c3a36a225 | |||
| 8936883a40 | |||
| c3af273401 | |||
| 77f3a522eb | |||
| de2a2c9013 | |||
| a78e610040 | |||
| 707eec0098 | |||
| 8c89a33ecf | |||
| 6f82c6af7f |
@@ -1,102 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# BRIEF: Build and deploy MokoGitea via SSH to production server
|
|
||||||
|
|
||||||
name: Deploy MokoGitea
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version tag (e.g. v05.00.00)'
|
|
||||||
required: true
|
|
||||||
default: 'latest'
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-mokogitea
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: git.mokoconsulting.tech
|
|
||||||
IMAGE: mokoconsulting/mokogitea
|
|
||||||
DEPLOY_HOST: git.mokoconsulting.tech
|
|
||||||
DEPLOY_PORT: 2918
|
|
||||||
DEPLOY_USER: mokoconsulting
|
|
||||||
COMPOSE_DIR: /opt/gitea
|
|
||||||
SOURCE_DIR: /opt/gitea/source
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Determine version tag
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo "tag=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Deploy via SSH
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
||||||
VERSION_TAG: ${{ steps.version.outputs.tag }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
|
|
||||||
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
|
|
||||||
|
|
||||||
$SSH_CMD "echo 'SSH connected'"
|
|
||||||
|
|
||||||
# Clone or update source
|
|
||||||
$SSH_CMD "
|
|
||||||
set -e
|
|
||||||
if [ ! -d ${{ env.SOURCE_DIR }}/.git ]; then
|
|
||||||
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${{ env.SOURCE_DIR }}
|
|
||||||
fi
|
|
||||||
cd ${{ env.SOURCE_DIR }}
|
|
||||||
git fetch origin main
|
|
||||||
git reset --hard origin/main
|
|
||||||
"
|
|
||||||
|
|
||||||
# Build Docker image on server (standard root layout, -p 1 for 12GB server)
|
|
||||||
$SSH_CMD "
|
|
||||||
set -e
|
|
||||||
cd ${{ env.SOURCE_DIR }}
|
|
||||||
docker build \
|
|
||||||
--build-arg GOFLAGS='-p 1' \
|
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${VERSION_TAG} \
|
|
||||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
|
||||||
-f Dockerfile .
|
|
||||||
"
|
|
||||||
|
|
||||||
# Update compose and restart
|
|
||||||
$SSH_CMD "
|
|
||||||
set -e
|
|
||||||
cd ${{ env.COMPOSE_DIR }}
|
|
||||||
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${VERSION_TAG}|' docker-compose.yml
|
|
||||||
docker compose up -d gitea
|
|
||||||
"
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
$SSH_CMD "
|
|
||||||
for i in 1 2 3 4 5 6 7 8; do
|
|
||||||
sleep 15
|
|
||||||
if docker inspect --format='{{.State.Health.Status}}' gitea 2>/dev/null | grep -q healthy; then
|
|
||||||
echo 'Gitea is healthy!'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo \"Waiting... (attempt \$i/8)\"
|
|
||||||
done
|
|
||||||
echo 'Health check failed'
|
|
||||||
docker logs gitea --tail 20
|
|
||||||
exit 1
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: Verify
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
curl -sf https://git.mokoconsulting.tech/api/healthz && echo " — API healthy"
|
|
||||||
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
run: echo "::error::Deploy failed for ${{ steps.version.outputs.tag }}"
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: Architecture Decision Record (ADR)
|
||||||
|
about: Propose or document an architectural decision
|
||||||
|
title: '[ADR] '
|
||||||
|
labels: 'architecture, decision'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ADR Number
|
||||||
|
ADR-XXXX
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Proposed
|
||||||
|
- [ ] Accepted
|
||||||
|
- [ ] Deprecated
|
||||||
|
- [ ] Superseded by ADR-XXXX
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Describe the issue or problem that motivates this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the architecture decision and provide rationale.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- List positive consequences
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- List negative consequences or trade-offs
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- List neutral aspects
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. Step 3
|
||||||
|
|
||||||
|
## Stakeholders
|
||||||
|
- **Decision Makers**: @user1, @user2
|
||||||
|
- **Consulted**: @user3, @user4
|
||||||
|
- **Informed**: team-name
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
### Architecture Diagram
|
||||||
|
```
|
||||||
|
[Add diagram or link]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Dependency 1
|
||||||
|
- Dependency 2
|
||||||
|
|
||||||
|
### Impact Analysis
|
||||||
|
- **Performance**: [Impact description]
|
||||||
|
- **Security**: [Impact description]
|
||||||
|
- **Scalability**: [Impact description]
|
||||||
|
- **Maintainability**: [Impact description]
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Performance tests
|
||||||
|
- [ ] Security tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [ ] Architecture documentation updated
|
||||||
|
- [ ] API documentation updated
|
||||||
|
- [ ] Developer guide updated
|
||||||
|
- [ ] Runbook created
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
Describe how to migrate from current state to new architecture.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
Describe how to rollback if issues occur.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Proposal Date**:
|
||||||
|
- **Decision Date**:
|
||||||
|
- **Implementation Start**:
|
||||||
|
- **Expected Completion**:
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related ADRs:
|
||||||
|
- External resources:
|
||||||
|
- RFCs:
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Aligns with enterprise architecture principles
|
||||||
|
- [ ] Security implications reviewed
|
||||||
|
- [ ] Performance implications reviewed
|
||||||
|
- [ ] Cost implications reviewed
|
||||||
|
- [ ] Compliance requirements met
|
||||||
|
- [ ] Team consensus achieved
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or issue with the project
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
|
||||||
|
- **Version**: [e.g., 1.2.3]
|
||||||
|
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1]
|
||||||
|
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
|
||||||
|
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
|
||||||
|
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
If you have suggestions on how to fix the issue, please describe them here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar issues before creating this one
|
||||||
|
- [ ] I have provided all the requested information
|
||||||
|
- [ ] I have tested this on the latest stable version
|
||||||
|
- [ ] I have checked the documentation and couldn't find a solution
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💼 Enterprise Support
|
||||||
|
url: https://mokoconsulting.tech/enterprise
|
||||||
|
about: Enterprise-level support and consultation services
|
||||||
|
- name: 💬 Ask a Question
|
||||||
|
url: https://mokoconsulting.tech/
|
||||||
|
about: Get help or ask questions through our website
|
||||||
|
- name: 📚 MokoStandards Documentation
|
||||||
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
about: View our coding standards and best practices
|
||||||
|
- name: 🔒 Report a Security Vulnerability
|
||||||
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
|
- name: 💡 Community Discussions
|
||||||
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
|
about: Join community discussions and Q&A
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report an issue with documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: 'documentation'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
<!-- Specify the file, page, or section with the issue -->
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
- [ ] Typo or grammar error
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing documentation
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Broken links
|
||||||
|
- [ ] Missing examples
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Clearly describe the documentation issue -->
|
||||||
|
|
||||||
|
## Current Content
|
||||||
|
<!-- Quote or describe the current documentation (if applicable) -->
|
||||||
|
```
|
||||||
|
Current text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Provide your suggestion for how to improve the documentation -->
|
||||||
|
```
|
||||||
|
Suggested text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
|
## Standards Alignment
|
||||||
|
- [ ] Follows MokoStandards documentation guidelines
|
||||||
|
- [ ] Uses en_US/en_GB localization
|
||||||
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar documentation issues
|
||||||
|
- [ ] I have provided a clear description
|
||||||
|
- [ ] I have suggested an improvement (if applicable)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem or Use Case
|
||||||
|
Describe the problem this feature would solve or the use case it addresses.
|
||||||
|
Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Describe how this feature would benefit users:
|
||||||
|
- Who would use this feature?
|
||||||
|
- What problems does it solve?
|
||||||
|
- What value does it add?
|
||||||
|
|
||||||
|
## Implementation Details (Optional)
|
||||||
|
If you have ideas about how this could be implemented, share them here:
|
||||||
|
- Technical approach
|
||||||
|
- Files/components that might need changes
|
||||||
|
- Any concerns or challenges you foresee
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
|
## Relevant Standards
|
||||||
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
|
- [ ] Localization (en_US/en_GB)
|
||||||
|
- [ ] Security best practices
|
||||||
|
- [ ] Code quality standards
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar feature requests before creating this one
|
||||||
|
- [ ] I have clearly described the use case and benefits
|
||||||
|
- [ ] I have considered alternative solutions
|
||||||
|
- [ ] This feature aligns with the project's goals and scope
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about usage, features, or best practices
|
||||||
|
title: '[QUESTION] '
|
||||||
|
labels: ['question']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
**Your question:**
|
||||||
|
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**What are you trying to accomplish?**
|
||||||
|
|
||||||
|
|
||||||
|
**What have you already tried?**
|
||||||
|
|
||||||
|
|
||||||
|
**Category**:
|
||||||
|
- [ ] Script usage
|
||||||
|
- [ ] Configuration
|
||||||
|
- [ ] Workflow setup
|
||||||
|
- [ ] Documentation interpretation
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] Integration
|
||||||
|
- [ ] Other: __________
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
**Your setup**:
|
||||||
|
- Operating System:
|
||||||
|
- Version:
|
||||||
|
|
||||||
|
## What You've Researched
|
||||||
|
|
||||||
|
**Documentation reviewed**:
|
||||||
|
- [ ] README.md
|
||||||
|
- [ ] Project documentation
|
||||||
|
- [ ] Other (specify): __________
|
||||||
|
|
||||||
|
**Similar issues/questions found**:
|
||||||
|
- #
|
||||||
|
- #
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
**What result are you hoping for?**
|
||||||
|
|
||||||
|
|
||||||
|
## Code/Configuration Samples
|
||||||
|
|
||||||
|
**Relevant code or configuration** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
**Any other relevant information:**
|
||||||
|
|
||||||
|
|
||||||
|
**Screenshots** (if helpful):
|
||||||
|
|
||||||
|
|
||||||
|
## Urgency
|
||||||
|
|
||||||
|
- [ ] Urgent (blocking work)
|
||||||
|
- [ ] Normal (can work on other things meanwhile)
|
||||||
|
- [ ] Low priority (just curious)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions
|
||||||
|
- [ ] I have reviewed relevant documentation
|
||||||
|
- [ ] I have provided sufficient context
|
||||||
|
- [ ] I have included code/configuration samples if relevant
|
||||||
|
- [ ] This is a genuine question (not a bug report or feature request)
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: Request for Comments (RFC)
|
||||||
|
about: Propose a significant change for community discussion
|
||||||
|
title: '[RFC] '
|
||||||
|
labels: 'rfc, discussion'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## RFC Summary
|
||||||
|
One-paragraph summary of the proposal.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why are we doing this? What use cases does it support? What is the expected outcome?
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
### Overview
|
||||||
|
Provide a detailed explanation of the proposed change.
|
||||||
|
|
||||||
|
### API Changes (if applicable)
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
function oldApi($param1) { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
function newApi($param1, $param2) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Experience Changes
|
||||||
|
Describe how users will interact with this change.
|
||||||
|
|
||||||
|
### Implementation Approach
|
||||||
|
High-level implementation strategy.
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
Why should we *not* do this?
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
What other designs have been considered? What is the impact of not doing this?
|
||||||
|
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
## Adoption Strategy
|
||||||
|
How will existing users adopt this? Is this a breaking change?
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
```bash
|
||||||
|
# Steps to migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecation Timeline
|
||||||
|
- **Announcement**:
|
||||||
|
- **Deprecation**:
|
||||||
|
- **Removal**:
|
||||||
|
|
||||||
|
## Unresolved Questions
|
||||||
|
- Question 1
|
||||||
|
- Question 2
|
||||||
|
|
||||||
|
## Future Possibilities
|
||||||
|
What future work does this enable?
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
### Performance
|
||||||
|
Expected performance impact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Security considerations and implications.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Backward Compatible**: [Yes / No]
|
||||||
|
- **Breaking Changes**: [List]
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
Long-term maintenance considerations.
|
||||||
|
|
||||||
|
## Community Input
|
||||||
|
### Stakeholders
|
||||||
|
- [ ] Core team
|
||||||
|
- [ ] Module developers
|
||||||
|
- [ ] End users
|
||||||
|
- [ ] Enterprise customers
|
||||||
|
|
||||||
|
### Feedback Period
|
||||||
|
**Duration**: [e.g., 2 weeks]
|
||||||
|
**Deadline**: [date]
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
### Phase 1: Design
|
||||||
|
- [ ] RFC discussion
|
||||||
|
- [ ] Design finalization
|
||||||
|
- [ ] Approval
|
||||||
|
|
||||||
|
### Phase 2: Implementation
|
||||||
|
- [ ] Core implementation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
|
### Phase 3: Release
|
||||||
|
- [ ] Beta release
|
||||||
|
- [ ] Feedback collection
|
||||||
|
- [ ] Stable release
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
How will we measure success?
|
||||||
|
- Metric 1
|
||||||
|
- Metric 2
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related RFCs:
|
||||||
|
- External documentation:
|
||||||
|
- Prior art:
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
1. Question 1?
|
||||||
|
2. Question 2?
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Security Vulnerability Report
|
||||||
|
about: Report a security vulnerability (use only for non-critical issues)
|
||||||
|
title: '[SECURITY] '
|
||||||
|
labels: 'security'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
|
**For critical security vulnerabilities, DO NOT use this template.**
|
||||||
|
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
|
||||||
|
|
||||||
|
Use this template only for:
|
||||||
|
- Security improvements
|
||||||
|
- Non-critical security suggestions
|
||||||
|
- Security documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Issue
|
||||||
|
|
||||||
|
**Severity**:
|
||||||
|
<!-- Low, Medium, or informational only -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Describe the security concern or improvement suggestion -->
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
<!-- List the affected files, features, or components -->
|
||||||
|
|
||||||
|
## Suggested Mitigation
|
||||||
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] SPDX license identifiers
|
||||||
|
- [ ] Secret management
|
||||||
|
- [ ] Dependency security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the security concern -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] This is NOT a critical vulnerability requiring private disclosure
|
||||||
|
- [ ] I have reviewed the SECURITY.md policy
|
||||||
|
- [ ] I have provided sufficient detail for evaluation
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: ".mokogitea Test Template"
|
||||||
|
about: "Verify .mokogitea issue templates work"
|
||||||
|
labels: ["test"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This template was loaded from `.mokogitea/ISSUE_TEMPLATE/`.
|
||||||
|
|
||||||
|
If you can see this, the `.mokogitea` dot-folder feature is working.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
about: Request or track a version change
|
||||||
|
title: '[VERSION] '
|
||||||
|
labels: 'version, type: version'
|
||||||
|
assignees: 'jmiller'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
**Current version**: <!-- e.g., 01.02.03 -->
|
||||||
|
**Requested version**: <!-- e.g., 01.03.00 -->
|
||||||
|
**Change type**: <!-- patch / minor / major -->
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
<!-- Why is this version bump needed? -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] README.md `VERSION:` field updated
|
||||||
|
- [ ] CHANGELOG.md entry added
|
||||||
|
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
|
||||||
|
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# BRIEF: Build MokoGitea Docker image, push to registry, and deploy
|
||||||
|
|
||||||
|
name: Deploy MokoGitea
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version tag (e.g. v1.26.1-moko.04.00.00)'
|
||||||
|
required: true
|
||||||
|
default: 'latest'
|
||||||
|
environment:
|
||||||
|
description: 'Target environment'
|
||||||
|
required: true
|
||||||
|
default: 'dev'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- production
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-mokogitea
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.mokoconsulting.tech
|
||||||
|
IMAGE: mokoconsulting/mokogitea
|
||||||
|
DEPLOY_HOST: git.mokoconsulting.tech
|
||||||
|
DEPLOY_PORT: 2918
|
||||||
|
DEPLOY_USER: mokoconsulting
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Determine settings
|
||||||
|
id: config
|
||||||
|
run: |
|
||||||
|
VERSION="${{ github.event.inputs.version }}"
|
||||||
|
ENV="${{ github.event.inputs.environment }}"
|
||||||
|
|
||||||
|
if [ "$ENV" = "production" ]; then
|
||||||
|
echo "compose_dir=/opt/gitea" >> $GITHUB_OUTPUT
|
||||||
|
echo "container=mokogitea" >> $GITHUB_OUTPUT
|
||||||
|
echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=main" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "container=mokogitea-dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build, push, and deploy via SSH
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
TAG: ${{ steps.config.outputs.tag }}
|
||||||
|
BRANCH: ${{ steps.config.outputs.branch }}
|
||||||
|
SOURCE_DIR: ${{ steps.config.outputs.source_dir }}
|
||||||
|
COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }}
|
||||||
|
CONTAINER: ${{ steps.config.outputs.container }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
SSH_CMD="ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}"
|
||||||
|
|
||||||
|
$SSH_CMD "echo 'SSH connected'"
|
||||||
|
|
||||||
|
# Pull latest source
|
||||||
|
$SSH_CMD "
|
||||||
|
set -e
|
||||||
|
if [ ! -d ${SOURCE_DIR}/.git ]; then
|
||||||
|
git clone -b ${BRANCH} https://git.mokoconsulting.tech/MokoConsulting/MokoGitea.git ${SOURCE_DIR}
|
||||||
|
fi
|
||||||
|
cd ${SOURCE_DIR}
|
||||||
|
git fetch origin ${BRANCH}
|
||||||
|
git reset --hard origin/${BRANCH}
|
||||||
|
"
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
$SSH_CMD "
|
||||||
|
set -e
|
||||||
|
cd ${SOURCE_DIR}
|
||||||
|
docker build --no-cache --build-arg GOFLAGS='-p 1' \
|
||||||
|
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG} \
|
||||||
|
--tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \
|
||||||
|
-f Dockerfile .
|
||||||
|
"
|
||||||
|
|
||||||
|
# Push to container registry
|
||||||
|
$SSH_CMD "
|
||||||
|
set -e
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${TAG}
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
|
"
|
||||||
|
|
||||||
|
# Update compose and restart
|
||||||
|
$SSH_CMD "
|
||||||
|
set -e
|
||||||
|
cd ${COMPOSE_DIR}
|
||||||
|
sed -i 's|${{ env.IMAGE }}:[^ ]*|${{ env.IMAGE }}:${TAG}|' docker-compose.yml
|
||||||
|
docker compose up -d ${CONTAINER}
|
||||||
|
"
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
$SSH_CMD "
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
sleep 15
|
||||||
|
if docker inspect --format='{{.State.Health.Status}}' ${CONTAINER} 2>/dev/null | grep -q healthy; then
|
||||||
|
echo 'Container healthy!'
|
||||||
|
docker inspect --format='Image: {{.Config.Image}}' ${CONTAINER}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo \"Waiting... (attempt \$i/8)\"
|
||||||
|
done
|
||||||
|
echo 'Health check failed'
|
||||||
|
docker logs ${CONTAINER} --tail 20
|
||||||
|
exit 1
|
||||||
|
"
|
||||||
|
|
||||||
|
- name: Verify
|
||||||
|
run: |
|
||||||
|
sleep 5
|
||||||
|
curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " — API healthy"
|
||||||
|
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
run: echo "::error::Deploy failed for ${{ steps.config.outputs.tag }}"
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# BRIEF: Auto-build RC release on PR to main, update RC update stream
|
||||||
|
|
||||||
|
name: "PR RC Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
REGISTRY: git.mokoconsulting.tech
|
||||||
|
IMAGE: mokoconsulting/mokogitea
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rc-release:
|
||||||
|
name: Build RC Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check target branch
|
||||||
|
id: guard
|
||||||
|
env:
|
||||||
|
BASE_BRANCH: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
echo "PR target: ${BASE_BRANCH}"
|
||||||
|
if [ "$BASE_BRANCH" != "main" ]; then
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Skipping RC — only for PRs targeting main"
|
||||||
|
else
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout PR branch
|
||||||
|
if: steps.guard.outputs.skip != 'true'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine RC version
|
||||||
|
if: steps.guard.outputs.skip != 'true'
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
BASE_VERSION=$(sed -n 's/.*<version>\(.*\)<\/version>.*/\1/p' updates.xml | head -1)
|
||||||
|
[ -z "$BASE_VERSION" ] && BASE_VERSION="04.00.00"
|
||||||
|
RC_VERSION="${BASE_VERSION}-rc.${PR_NUMBER}"
|
||||||
|
RC_TAG="v1.26.1-moko.${RC_VERSION}"
|
||||||
|
echo "version=$RC_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=$RC_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "RC version: $RC_VERSION (tag: $RC_TAG)"
|
||||||
|
|
||||||
|
- name: Update updates.xml RC channel
|
||||||
|
if: steps.guard.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
PR_NUM: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG="${REGISTRY}/${IMAGE}:${RC_TAG}"
|
||||||
|
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import os, re
|
||||||
|
|
||||||
|
rc_version = os.environ["RC_VERSION"]
|
||||||
|
rc_tag = os.environ["RC_TAG"]
|
||||||
|
pr_url = os.environ["PR_URL"]
|
||||||
|
pr_num = os.environ["PR_NUM"]
|
||||||
|
docker_tag = os.environ["REGISTRY"] + "/" + os.environ["IMAGE"] + ":" + rc_tag
|
||||||
|
|
||||||
|
entry = f""" <update>
|
||||||
|
<name>MokoGitea</name>
|
||||||
|
<description>MokoGitea RC from PR #{pr_num}</description>
|
||||||
|
<element>mokogitea</element>
|
||||||
|
<type>application</type>
|
||||||
|
<version>{rc_version}</version>
|
||||||
|
<client>server</client>
|
||||||
|
<tags><tag>rc</tag></tags>
|
||||||
|
<infourl title="MokoGitea RC">{pr_url}</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type="full" format="docker">{docker_tag}</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256></sha256>
|
||||||
|
<targetplatform name="mokogitea" version="((1\\.25\\.)|(1\\.26\\.))" />
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
</update>"""
|
||||||
|
|
||||||
|
content = open("updates.xml").read()
|
||||||
|
# Remove existing RC entry
|
||||||
|
content = re.sub(
|
||||||
|
r"\s*<update>[\s\S]*?<tag>rc</tag>[\s\S]*?</update>",
|
||||||
|
"",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
# Insert before </updates>
|
||||||
|
content = content.replace("</updates>", entry + "\n</updates>")
|
||||||
|
open("updates.xml", "w").write(content)
|
||||||
|
print(f"Updated updates.xml with RC entry: {rc_version}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
- name: Create RC release
|
||||||
|
if: steps.guard.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
RC_TAG: ${{ steps.version.outputs.tag }}
|
||||||
|
RC_VERSION: ${{ steps.version.outputs.version }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
API_BASE: https://${{ env.REGISTRY }}/api/v1/repos/${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
# Delete existing RC release/tag if present
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/releases/tags/${RC_TAG}" 2>/dev/null || true
|
||||||
|
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/tags/${RC_TAG}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create prerelease
|
||||||
|
python3 << PYEOF
|
||||||
|
import json, os, urllib.request
|
||||||
|
|
||||||
|
api = os.environ["API_BASE"]
|
||||||
|
token = os.environ["GITEA_TOKEN"]
|
||||||
|
payload = json.dumps({
|
||||||
|
"tag_name": os.environ["RC_TAG"],
|
||||||
|
"target_commitish": os.environ["HEAD_SHA"],
|
||||||
|
"name": f"RC: {os.environ['PR_TITLE']}",
|
||||||
|
"body": f"Release candidate from PR #{os.environ['PR_NUMBER']}\n\nPR: {os.environ['PR_URL']}\nDocker: docker pull {os.environ['REGISTRY']}/{os.environ['IMAGE']}:{os.environ['RC_TAG']}",
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": True,
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{api}/releases",
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
print(f"Created RC release: {result.get('tag_name')}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
- name: Commit updates.xml
|
||||||
|
if: steps.guard.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||||
|
PR_NUM: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
git config user.name "MokoGitea Bot"
|
||||||
|
git config user.email "deploy@mokoconsulting.tech"
|
||||||
|
git add updates.xml
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to updates.xml"
|
||||||
|
else
|
||||||
|
git commit -m "chore(ci): update RC stream for PR #${PR_NUM}"
|
||||||
|
git push origin "HEAD:${HEAD_REF}" || echo "Push failed"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Test workflow to verify .mokogitea/ directory is discovered
|
||||||
|
name: Test .mokogitea workflows
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Verify .mokogitea
|
||||||
|
run: echo "This workflow ran from .mokogitea/workflows/ — feature works!"
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# BRIEF: Sync upstream Gitea bug fixes into MokoGitea issue tracker
|
||||||
|
|
||||||
|
name: Upstream Bug Sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 8 * * *' # daily at 08:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
days_back:
|
||||||
|
description: 'How many days back to scan (default: 7)'
|
||||||
|
required: false
|
||||||
|
default: '7'
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Sync upstream bugs
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
MOKOGITEA_URL: https://git.mokoconsulting.tech
|
||||||
|
MOKOGITEA_REPO: MokoConsulting/MokoGitea
|
||||||
|
UPSTREAM_BRANCH: release/v1.26
|
||||||
|
DAYS_BACK: ${{ github.event.inputs.days_back || '7' }}
|
||||||
|
run: |
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import json, os, re, sys, urllib.parse, urllib.request
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
GH_TOKEN = os.environ["GH_TOKEN"]
|
||||||
|
MOKO_TOKEN = os.environ["MOKOGITEA_TOKEN"]
|
||||||
|
MOKO_URL = os.environ["MOKOGITEA_URL"]
|
||||||
|
MOKO_REPO = os.environ["MOKOGITEA_REPO"]
|
||||||
|
BRANCH = os.environ["UPSTREAM_BRANCH"]
|
||||||
|
DAYS = int(os.environ.get("DAYS_BACK", "7"))
|
||||||
|
|
||||||
|
# Label IDs in MokoGitea
|
||||||
|
LABELS = {
|
||||||
|
"type_bug": 5757, "upstream": 5758, "security": 5032,
|
||||||
|
"critical": 5018, "high": 5019, "medium": 5020, "low": 5021,
|
||||||
|
}
|
||||||
|
|
||||||
|
def gh_get(url):
|
||||||
|
req = urllib.request.Request(url, headers={
|
||||||
|
"Authorization": f"token {GH_TOKEN}",
|
||||||
|
"Accept": "application/vnd.github.v3+json",
|
||||||
|
})
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
|
||||||
|
def moko_get(path):
|
||||||
|
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}", headers={
|
||||||
|
"Authorization": f"token {MOKO_TOKEN}",
|
||||||
|
})
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
|
||||||
|
def moko_post(path, data):
|
||||||
|
payload = json.dumps(data).encode()
|
||||||
|
req = urllib.request.Request(f"{MOKO_URL}/api/v1/{path}",
|
||||||
|
data=payload, method="POST", headers={
|
||||||
|
"Authorization": f"token {MOKO_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
with urllib.request.urlopen(req) as r:
|
||||||
|
return json.loads(r.read())
|
||||||
|
|
||||||
|
# ── Step 1: Find recently merged upstream PRs ──
|
||||||
|
since = (datetime.now(timezone.utc) - timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
query = f"repo:go-gitea/gitea is:pr is:merged base:{BRANCH} merged:>={since}"
|
||||||
|
encoded = urllib.parse.quote(query)
|
||||||
|
print(f"Scanning: {query}")
|
||||||
|
|
||||||
|
result = gh_get(f"https://api.github.com/search/issues?q={encoded}&per_page=100&sort=updated&order=desc")
|
||||||
|
total = result["total_count"]
|
||||||
|
print(f"Found {total} merged PRs in the last {DAYS} days")
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
print("Nothing to sync.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ── Step 2: Filter for bug/security fixes ──
|
||||||
|
bugs = []
|
||||||
|
for pr in result["items"]:
|
||||||
|
title = pr["title"]
|
||||||
|
label_names = [l["name"].lower() for l in pr.get("labels", [])]
|
||||||
|
is_fix = title.lower().startswith("fix")
|
||||||
|
is_security = any("security" in l for l in label_names) or "[security]" in title.lower()
|
||||||
|
is_bug = any("bug" in l for l in label_names)
|
||||||
|
|
||||||
|
if not (is_fix or is_security or is_bug):
|
||||||
|
continue
|
||||||
|
|
||||||
|
refs = re.findall(r"#(\d+)", title)
|
||||||
|
severity = "critical" if is_security and "[security]" in title.lower() else \
|
||||||
|
"high" if is_security else "medium"
|
||||||
|
|
||||||
|
bugs.append({
|
||||||
|
"number": pr["number"], "title": title, "url": pr["html_url"],
|
||||||
|
"severity": severity, "is_security": is_security, "refs": refs,
|
||||||
|
"merged": pr.get("pull_request", {}).get("merged_at", "")[:10],
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"Filtered to {len(bugs)} bug/security fixes")
|
||||||
|
if not bugs:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ── Step 3: Collect already-tracked PR numbers ──
|
||||||
|
tracked = set()
|
||||||
|
for state in ["open", "closed"]:
|
||||||
|
try:
|
||||||
|
issues = moko_get(f"repos/{MOKO_REPO}/issues?state={state}&type=issues&limit=50&labels=upstream")
|
||||||
|
for iss in issues:
|
||||||
|
text = (iss.get("body") or "") + " " + (iss.get("title") or "")
|
||||||
|
tracked.update(re.findall(r"(?:#|/pull/)(\d{4,})", text))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"Already tracked: {len(tracked)} upstream PRs")
|
||||||
|
|
||||||
|
# ── Step 4: Create issues for new bugs ──
|
||||||
|
created = skipped = errors = 0
|
||||||
|
for bug in bugs:
|
||||||
|
if any(r in tracked for r in bug["refs"]):
|
||||||
|
print(f" SKIP #{bug['number']}: {bug['title'][:55]} (tracked)")
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
labels = [LABELS["type_bug"], LABELS["upstream"], LABELS[bug["severity"]]]
|
||||||
|
if bug["is_security"]:
|
||||||
|
labels.append(LABELS["security"])
|
||||||
|
|
||||||
|
body = (
|
||||||
|
f"## Summary\n\n"
|
||||||
|
f"Upstream bug fix merged into `{BRANCH}`.\n\n"
|
||||||
|
f"## Upstream Reference\n\n"
|
||||||
|
f"- PR: {bug['url']}\n"
|
||||||
|
f"- Merged: {bug['merged']}\n"
|
||||||
|
f"- Branch: {BRANCH}\n\n"
|
||||||
|
f"## Severity: {bug['severity'].title()}"
|
||||||
|
f"{' (security)' if bug['is_security'] else ''}\n\n"
|
||||||
|
f"## Action\n\n"
|
||||||
|
f"Cherry-pick from upstream `{BRANCH}` branch.\n\n"
|
||||||
|
f"---\n"
|
||||||
|
f"*Auto-created by upstream-bug-sync workflow*\n"
|
||||||
|
f"*Authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>*"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
iss = moko_post(f"repos/{MOKO_REPO}/issues", {
|
||||||
|
"title": bug["title"], "body": body, "labels": labels,
|
||||||
|
})
|
||||||
|
print(f" CREATED #{iss['number']}: {bug['title'][:55]}")
|
||||||
|
created += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR #{bug['number']}: {e}")
|
||||||
|
errors += 1
|
||||||
|
|
||||||
|
print(f"\n=== Done: {created} created, {skipped} skipped, {errors} errors ===")
|
||||||
|
PYEOF
|
||||||
@@ -4,6 +4,59 @@ This changelog goes through the changes that have been made in each release
|
|||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
|
## [v1.26.1-moko.04.00.00] - 2026-05-24
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Backport 12 upstream v1.26.2 security fixes:
|
||||||
|
* golang.org/x/net v0.55.0 security update (#140)
|
||||||
|
* Token scope enforcement on raw/media/attachment downloads (#141)
|
||||||
|
* OAuth PKCE hardening and refresh token replay protection (#142)
|
||||||
|
* Wiki git write and LFS token access enforcement (#143)
|
||||||
|
* Public-only token filtering in API queries (#144)
|
||||||
|
* Reading permission fix (#145)
|
||||||
|
* Artifact signature payload hardening (#146)
|
||||||
|
* AWS credentials encryption (#161)
|
||||||
|
* Mermaid v11.15.0 security update (#162)
|
||||||
|
* Composer package permission check (#164)
|
||||||
|
* BUGFIXES
|
||||||
|
* fix(actions): nil pointer dereference in concurrency during PR creation (#136)
|
||||||
|
* fix(ui): actions runs list broken row layout — CSS class mismatch (#138)
|
||||||
|
* fix: scheduled action panic with null event payload (upstream #37459)
|
||||||
|
* fix: treat email addresses case-insensitively (upstream #37600)
|
||||||
|
* fix: .mod lexer panic — removed invalid AMPL mapping
|
||||||
|
* fix: remove unused setting import in action.go
|
||||||
|
* fix: restore Permission field access in context middleware
|
||||||
|
* FEATURES
|
||||||
|
* Joomla-style updates.xml with channel selection (stable/dev/security/rc)
|
||||||
|
* Update checker reads from updates.xml with configurable CHANNEL setting
|
||||||
|
* Admin dashboard shows update banner with channel name and docker pull command
|
||||||
|
* Upstream bug sync workflow — daily automated issue creation from release/v1.26
|
||||||
|
* PR RC release workflow — auto-build RC on PR to main
|
||||||
|
* INFRASTRUCTURE
|
||||||
|
* New 3-part versioning: v{upstream}-moko.{major}.{minor}.{patch}
|
||||||
|
* Branding updates: error pages, home page, settings link to MokoGitea
|
||||||
|
* Deploy workflow updated for new version format
|
||||||
|
* PROCESS
|
||||||
|
* Created `type: bug` and `upstream` labels for automated issue tracking
|
||||||
|
* Deduplicated 19 duplicate feature request issues
|
||||||
|
* Closed 24 upstream bug/security issues after backporting
|
||||||
|
|
||||||
|
## [MokoGitea Unreleased]
|
||||||
|
|
||||||
|
* FEATURES
|
||||||
|
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, and set assignees across multiple issues in a single request (#21)
|
||||||
|
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/labels`
|
||||||
|
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/state`
|
||||||
|
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/milestone`
|
||||||
|
* `POST /api/v1/repos/{owner}/{repo}/issues/bulk/assignees`
|
||||||
|
* Partial-failure support: returns per-issue success/failure map
|
||||||
|
* INFRASTRUCTURE
|
||||||
|
* Grafana: Standardized kiosk header across all 14 playlist dashboards — each now shows dashboard name, kiosk link, terminal/exit/switch instructions
|
||||||
|
* PROCESS
|
||||||
|
* Reopened 9 closed issues lacking documented testing proof (#3, #5, #38, #41, #70, #74, #75, #76, #78)
|
||||||
|
* Created `pending: testing` label for features awaiting verification
|
||||||
|
* Established policy: issues must not be closed without documented testing proof
|
||||||
|
|
||||||
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
||||||
|
|
||||||
* BUGFIXES
|
* BUGFIXES
|
||||||
@@ -5799,3 +5852,4 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
## Archived releases
|
## Archived releases
|
||||||
|
|
||||||
* [CHANGELOG-archived.md](CHANGELOG-archived.md)
|
* [CHANGELOG-archived.md](CHANGELOG-archived.md)
|
||||||
|
# PR RC Workflow Test
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
# Community governance and review process
|
|
||||||
|
|
||||||
This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Code review
|
|
||||||
|
|
||||||
### Milestone
|
|
||||||
|
|
||||||
A PR should only be assigned to a milestone if it will likely be merged into the given version. \
|
|
||||||
PRs without a milestone may not be merged.
|
|
||||||
|
|
||||||
### Labels
|
|
||||||
|
|
||||||
Almost all labels used inside Gitea can be classified as one of the following:
|
|
||||||
|
|
||||||
- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI.
|
|
||||||
- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually.
|
|
||||||
- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one).
|
|
||||||
- `issue/…` / `lgtm/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `lgtm/need 2`
|
|
||||||
|
|
||||||
Every PR should be labeled correctly with every label that applies.
|
|
||||||
|
|
||||||
There are also some labels that will be managed automatically.\
|
|
||||||
In particular, these are
|
|
||||||
|
|
||||||
- the amount of pending required approvals
|
|
||||||
- has all `backport`s or needs a manual backport
|
|
||||||
|
|
||||||
### Reviewing PRs
|
|
||||||
|
|
||||||
Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest.
|
|
||||||
|
|
||||||
#### For reviewers
|
|
||||||
|
|
||||||
- **Verification**: Verify that the PR accurately reflects the changes, and verify that the tests and documentation are complete and aligned with the implementation.
|
|
||||||
- **Actionable feedback**: Say what should change and why, and distinguish required changes from optional suggestions.
|
|
||||||
- **Feedback**: Focus feedback on the issue itself and avoid comments about the contributor's abilities.
|
|
||||||
- **Request changes**: If you request changes (i.e., block a PR), give a clear rationale and, whenever possible, a concrete path to resolution.
|
|
||||||
- **Approval**: Only approve a PR when you are fully satisfied with its current state - "rubber-stamp" approvals need to be highlighted as such.
|
|
||||||
|
|
||||||
### Getting PRs merged
|
|
||||||
|
|
||||||
Changes to Gitea must be reviewed before they are accepted, including changes from owners and maintainers. The exception is critical bugs that prevent Gitea from compiling or starting.
|
|
||||||
|
|
||||||
We require two maintainer approvals for every PR. When that is satisfied, your PR gets the `lgtm/done` label. After that, you mainly fix merge conflicts and respond to or implement maintainer requests; maintainers drive getting the PR merged.
|
|
||||||
|
|
||||||
If a PR has `lgtm/done`, no open discussions, and no merge conflicts, any maintainer may add `reviewed/wait-merge`. That puts the PR in the merge queue. PRs are merged from the queue in the order of this list:
|
|
||||||
|
|
||||||
<https://github.com/go-gitea/gitea/pulls?q=is%3Apr+label%3Areviewed%2Fwait-merge+sort%3Acreated-asc+is%3Aopen>
|
|
||||||
|
|
||||||
Gitea uses its own tool, <https://github.com/GiteaBot/gitea-backporter>, to automate parts of the review process. The backporter:
|
|
||||||
|
|
||||||
- Creates a backport PR when needed after the initial PR merges.
|
|
||||||
- Removes the PR from the merge queue after it merges.
|
|
||||||
- Keeps the oldest branch in the merge queue up to date with merges.
|
|
||||||
|
|
||||||
### Final call
|
|
||||||
|
|
||||||
If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment.
|
|
||||||
|
|
||||||
After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously.
|
|
||||||
|
|
||||||
However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author).
|
|
||||||
|
|
||||||
### Commit messages
|
|
||||||
|
|
||||||
Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear.
|
|
||||||
|
|
||||||
The final commit message should not hedge: replace phrases like `hopefully, <x> won't happen anymore` with definite wording.
|
|
||||||
|
|
||||||
#### PR Co-authors
|
|
||||||
|
|
||||||
A person counts as a PR co-author once they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. Mergers must remove such false-positive co-authors when writing the squash message. Every true co-author must remain in the commit message.
|
|
||||||
|
|
||||||
#### PRs targeting `main`
|
|
||||||
|
|
||||||
The commit message of PRs targeting `main` is always
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$PR_TITLE ($PR_INDEX)
|
|
||||||
|
|
||||||
$REWRITTEN_PR_SUMMARY
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Backport PRs
|
|
||||||
|
|
||||||
The commit message of backport PRs is always
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX)
|
|
||||||
|
|
||||||
$REWRITTEN_PR_SUMMARY
|
|
||||||
```
|
|
||||||
|
|
||||||
## Maintainers
|
|
||||||
|
|
||||||
We list [maintainers](../MAINTAINERS) so every PR gets proper review.
|
|
||||||
|
|
||||||
#### Review expectations
|
|
||||||
|
|
||||||
Every PR **must** be reviewed by at least two maintainers (or owners) before merge. **Exception:** after one week, refactoring PRs and documentation-only PRs need only one maintainer approval.
|
|
||||||
|
|
||||||
Maintainers are expected to spend time on code reviews.
|
|
||||||
|
|
||||||
#### Becoming a maintainer
|
|
||||||
|
|
||||||
A maintainer should already be a Gitea contributor with at least four merged PRs. To apply, use the [Discord](https://discord.gg/Gitea) `#develop` channel. Maintainer teams may also invite contributors.
|
|
||||||
|
|
||||||
#### Stepping down, advisors, and inactivity
|
|
||||||
|
|
||||||
If you cannot keep reviewing, apply to leave the maintainers team. You can join the [advisors team](https://github.com/orgs/go-gitea/teams/advisors); advisors who want to review again are welcome back as maintainers.
|
|
||||||
|
|
||||||
If a maintainer is inactive for more than three months and has not left the team, owners may move them to the advisors team.
|
|
||||||
|
|
||||||
#### Account security
|
|
||||||
|
|
||||||
For security, maintainers should enable 2FA and sign commits with GPG when possible:
|
|
||||||
|
|
||||||
- [Two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication)
|
|
||||||
- [Signing commits with GPG](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
|
|
||||||
|
|
||||||
Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication).
|
|
||||||
|
|
||||||
## Technical Oversight Committee (TOC)
|
|
||||||
|
|
||||||
At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
|
|
||||||
https://blog.gitea.com/quarterly-23q1/
|
|
||||||
|
|
||||||
### TOC election process
|
|
||||||
|
|
||||||
Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
|
|
||||||
A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
|
|
||||||
If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
|
|
||||||
|
|
||||||
The TOC is elected for one year, the TOC election happens yearly.
|
|
||||||
After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
|
|
||||||
If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
|
|
||||||
Refusals result in the person with the next highest vote getting the same choice.
|
|
||||||
As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
|
|
||||||
|
|
||||||
If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
|
|
||||||
|
|
||||||
### Current TOC members
|
|
||||||
|
|
||||||
- 2024-01-01 ~ 2024-12-31
|
|
||||||
- Company
|
|
||||||
- [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
|
|
||||||
- [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
|
|
||||||
- [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
|
|
||||||
- Community
|
|
||||||
- [6543](https://gitea.com/6543) <6543@obermui.de>
|
|
||||||
- [delvh](https://gitea.com/delvh) <dev.lh@web.de>
|
|
||||||
- [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
|
|
||||||
|
|
||||||
### Previous TOC/owners members
|
|
||||||
|
|
||||||
Here's the history of the owners and the time they served:
|
|
||||||
|
|
||||||
- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
|
|
||||||
- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
|
|
||||||
- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
|
|
||||||
- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
|
|
||||||
- [6543](https://gitea.com/6543) - 2023
|
|
||||||
- [John Olheiser](https://gitea.com/jolheiser) - 2023
|
|
||||||
- [Jason Song](https://gitea.com/wolfogre) - 2023
|
|
||||||
|
|
||||||
## Governance Compensation
|
|
||||||
|
|
||||||
Each member of the community elected TOC will be granted $500 each month as compensation for their work.
|
|
||||||
|
|
||||||
Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release.
|
|
||||||
|
|
||||||
These funds will come from community sources like the OpenCollective rather than directly from the company.
|
|
||||||
Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties.
|
|
||||||
Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself.
|
|
||||||
|
|
||||||
## TOC & Working groups
|
|
||||||
|
|
||||||
With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places.
|
|
||||||
|
|
||||||
The currently proposed groups are:
|
|
||||||
|
|
||||||
- **Core Group**: maintain the primary Gitea repository
|
|
||||||
- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc.
|
|
||||||
- **Documentation Group**: maintain related documents and repositories
|
|
||||||
- **Translation Group**: coordinate with translators and maintain translations
|
|
||||||
- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders.
|
|
||||||
TOC members need to review the roadmap every year and work together on the direction of the project.
|
|
||||||
|
|
||||||
When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion.
|
|
||||||
|
|
||||||
You can visit our roadmap on the wiki.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Backend development
|
|
||||||
|
|
||||||
This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
|
|
||||||
For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
|
|
||||||
You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
|
|
||||||
|
|
||||||
Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
|
|
||||||
Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies.
|
|
||||||
|
|
||||||
The `go.mod`, `go.sum` update needs to be justified as part of the PR description,
|
|
||||||
and must be verified by the reviewers and/or merger to always reference
|
|
||||||
an existing upstream commit.
|
|
||||||
|
|
||||||
## API v1
|
|
||||||
|
|
||||||
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
|
|
||||||
|
|
||||||
### GitHub API compatibility
|
|
||||||
|
|
||||||
Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \
|
|
||||||
If Gitea provides functionality that GitHub does not, a new endpoint can be created. \
|
|
||||||
If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \
|
|
||||||
Updating an existing API should not remove existing fields unless there is a really good reason to do so. \
|
|
||||||
The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned).
|
|
||||||
|
|
||||||
### Adding/Maintaining API routes
|
|
||||||
|
|
||||||
All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \
|
|
||||||
All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \
|
|
||||||
and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \
|
|
||||||
They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \
|
|
||||||
All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \
|
|
||||||
and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \
|
|
||||||
They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279).
|
|
||||||
|
|
||||||
### When to use what HTTP method
|
|
||||||
|
|
||||||
In general, HTTP methods are chosen as follows:
|
|
||||||
|
|
||||||
- **GET** endpoints return the requested object(s) and status **OK (200)**
|
|
||||||
- **DELETE** endpoints return the status **No Content (204)** and no content either
|
|
||||||
- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object
|
|
||||||
- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either
|
|
||||||
- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)**
|
|
||||||
|
|
||||||
### Requirements for API routes
|
|
||||||
|
|
||||||
All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required).
|
|
||||||
|
|
||||||
Endpoints returning lists must
|
|
||||||
|
|
||||||
- support pagination (`page` & `limit` options in query)
|
|
||||||
- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Frontend development
|
|
||||||
|
|
||||||
This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
For the frontend, we use [npm](https://www.npmjs.com/).
|
|
||||||
|
|
||||||
The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version.
|
|
||||||
|
|
||||||
## Design guideline
|
|
||||||
|
|
||||||
Depending on your change, please read the
|
|
||||||
|
|
||||||
- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend)
|
|
||||||
- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend)
|
|
||||||
- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# Release management
|
|
||||||
|
|
||||||
This document describes the release cycle, backports, versioning, and the release manager checklist. For everyday contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Backports and Frontports
|
|
||||||
|
|
||||||
### What is backported?
|
|
||||||
|
|
||||||
We backport PRs given the following circumstances:
|
|
||||||
|
|
||||||
1. Feature freeze is active, but `<version>-rc0` has not been released yet. Here, we backport as much as possible. <!-- TODO: Is that our definition with the new backport bot? -->
|
|
||||||
2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. <!-- TODO: Is that our definition with the new backport bot? -->
|
|
||||||
3. We never backport new features.
|
|
||||||
4. We never backport breaking changes except when
|
|
||||||
1. The breaking change has no effect on the vast majority of users
|
|
||||||
2. The component triggering the breaking change is marked as experimental
|
|
||||||
|
|
||||||
### How to backport?
|
|
||||||
|
|
||||||
In the past, it was necessary to manually backport your PRs. \
|
|
||||||
Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR
|
|
||||||
|
|
||||||
- does not have the label `backport/manual`
|
|
||||||
- has the label `backport/<version>`
|
|
||||||
|
|
||||||
The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself.
|
|
||||||
|
|
||||||
### Format of backport PRs
|
|
||||||
|
|
||||||
The title of backport PRs should be
|
|
||||||
|
|
||||||
```
|
|
||||||
<original PR title> (#<original pr number>)
|
|
||||||
```
|
|
||||||
|
|
||||||
The first two lines of the summary of the backporting PR should be
|
|
||||||
|
|
||||||
```
|
|
||||||
Backport #<original pr number>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
with the rest of the summary and labels matching the original PR.
|
|
||||||
|
|
||||||
### Frontports
|
|
||||||
|
|
||||||
Frontports behave exactly as described above for backports.
|
|
||||||
|
|
||||||
## Release Cycle
|
|
||||||
|
|
||||||
We use a release schedule so work, stabilization, and releases stay predictable.
|
|
||||||
|
|
||||||
### Cadence
|
|
||||||
|
|
||||||
- Aim for a major release about every three or four months.
|
|
||||||
- Roughly two or three months of general development, then about one month of testing and polish called the **release freeze**.
|
|
||||||
- *Starting with v1.26 the release cycle will be more predictable and follow a more regular schedule.*
|
|
||||||
|
|
||||||
### Release schedule
|
|
||||||
|
|
||||||
We will try to publish a new major version every three months:
|
|
||||||
|
|
||||||
- v1.26.0 in April 2026
|
|
||||||
- v1.27.0 in June 2026
|
|
||||||
- v1.28.0 in September 2026
|
|
||||||
- v1.29.0 in December 2026
|
|
||||||
|
|
||||||
#### How is the release handled?
|
|
||||||
- The release manager will tag the release candidate (e.g. `v1.26.0-rc0`) and publish it for testing in the **first week of the release month**.
|
|
||||||
- If there are no major issues, the release manager will check with the other maintainers and then tag the final release (e.g. `v1.26.0`) in the **one or two weeks following the release candidate**.
|
|
||||||
|
|
||||||
### Feature freeze
|
|
||||||
|
|
||||||
- Merge feature PRs before the freeze when you can.
|
|
||||||
- Feature PRs still open at the freeze move to the next milestone. Watch Discord for the freeze announcement.
|
|
||||||
- During the freeze, a **release branch** takes fixes backported from `main`. Release candidates ship for testing; the final release for that line is maintained from that branch.
|
|
||||||
|
|
||||||
### Patch releases
|
|
||||||
|
|
||||||
During a cycle we may ship patch releases for an older line. For example, if the latest release is v1.2, we can still publish v1.1.1 after v1.1.0.
|
|
||||||
|
|
||||||
### End of life (EOL)
|
|
||||||
|
|
||||||
We support per standard the last major release. For example, if the latest release is v1.26, we support v1.26 and v1.25, but not v1.24 anymore. We will only publish security fixes for the last major release, so if you are using an older release, please upgrade to a supported release as soon as possible.
|
|
||||||
Also we always try to support the latest on main branch, so if you are using the latest on main, you should be fine.
|
|
||||||
|
|
||||||
## Versions
|
|
||||||
|
|
||||||
Gitea has the `main` branch as a tip branch and has version branches
|
|
||||||
such as `release/v1.19`. `release/v1.19` is a release branch and we will
|
|
||||||
tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept
|
|
||||||
pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag,
|
|
||||||
after bringing the bug fix also to the main branch.
|
|
||||||
|
|
||||||
Since the `main` branch is a tip version, if you wish to use Gitea
|
|
||||||
in production, please download the latest release tag version. All the
|
|
||||||
branches will be protected via GitHub, all the PRs to every branch must
|
|
||||||
be reviewed by two maintainers and must pass the automatic tests.
|
|
||||||
|
|
||||||
## Releasing Gitea
|
|
||||||
|
|
||||||
- Let MAJOR, MINOR and PATCH be Major, Minor and Patch version numbers, PATCH should be rc1, rc2, 0, 1, ...... MAJOR.MINOR will be kept the same as milestones on github or gitea in future.
|
|
||||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
|
|
||||||
- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
|
|
||||||
- Create `-dev` tag as `git tag -s -F release.notes vMAJOR.MINOR.0-dev` and push the tag as `git push origin vMAJOR.MINOR.0-dev`.
|
|
||||||
- When CI has finished building tag then you have to create a new branch named `release/vMAJOR.MINOR`
|
|
||||||
- If it is bugfix version create PR for changelog on branch `release/vMAJOR.MINOR` and wait till it is reviewed and merged.
|
|
||||||
- Add a tag as `git tag -s -F release.notes vMAJOR.MINOR.PATCH`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
|
|
||||||
- And then push the tag as `git push origin vMAJOR.MINOR.$`. CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
|
|
||||||
- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
|
|
||||||
- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
|
|
||||||
- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed:
|
|
||||||
- bump the version of https://dl.gitea.com/gitea/version.json
|
|
||||||
- merge the blog post PR
|
|
||||||
- announce the release in discord `#announcements`
|
|
||||||
@@ -46,6 +46,7 @@ require (
|
|||||||
github.com/ethantkoenig/rupture v1.0.1
|
github.com/ethantkoenig/rupture v1.0.1
|
||||||
github.com/felixge/fgprof v0.9.5
|
github.com/felixge/fgprof v0.9.5
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/getkin/kin-openapi v0.138.0
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
@@ -86,7 +87,6 @@ require (
|
|||||||
github.com/msteinert/pam/v2 v2.1.0
|
github.com/msteinert/pam/v2 v2.1.0
|
||||||
github.com/nektos/act v0.2.63
|
github.com/nektos/act v0.2.63
|
||||||
github.com/niklasfasching/go-org v1.9.1
|
github.com/niklasfasching/go-org v1.9.1
|
||||||
github.com/olivere/elastic/v7 v7.0.32
|
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
@@ -110,17 +110,18 @@ require (
|
|||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
gitlab.com/gitlab-org/api/client-go v1.46.0
|
gitlab.com/gitlab-org/api/client-go v1.46.0
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.52.0
|
||||||
golang.org/x/image v0.38.0
|
golang.org/x/image v0.40.0
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.55.0
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.45.0
|
||||||
golang.org/x/text v0.35.0
|
golang.org/x/text v0.37.0
|
||||||
google.golang.org/grpc v1.79.3
|
google.golang.org/grpc v1.79.3
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/ini.v1 v1.67.1
|
gopkg.in/ini.v1 v1.67.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
modernc.org/sqlite v1.20.4
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
|
strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab
|
||||||
xorm.io/builder v0.3.13
|
xorm.io/builder v0.3.13
|
||||||
@@ -187,13 +188,14 @@ require (
|
|||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
github.com/fatih/color v1.19.0 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||||
github.com/getkin/kin-openapi v0.138.0 // indirect
|
|
||||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
|
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-webauthn/x v0.2.2 // indirect
|
github.com/go-webauthn/x v0.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.6 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
@@ -234,16 +236,20 @@ require (
|
|||||||
github.com/minio/minlz v1.1.0 // indirect
|
github.com/minio/minlz v1.1.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||||
|
github.com/oasdiff/yaml v0.0.9 // indirect
|
||||||
|
github.com/oasdiff/yaml3 v0.0.12 // indirect
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/olekukonko/errors v1.2.0 // indirect
|
github.com/olekukonko/errors v1.2.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.8 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
@@ -259,11 +265,13 @@ require (
|
|||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
|
github.com/smartystreets/assertions v1.1.1 // indirect
|
||||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/unknwon/com v1.0.1 // indirect
|
github.com/unknwon/com v1.0.1 // indirect
|
||||||
|
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
@@ -280,9 +288,9 @@ require (
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
golang.org/x/mod v0.35.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
lukechampine.com/uint128 v1.2.0 // indirect
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
@@ -292,7 +300,6 @@ require (
|
|||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/opt v0.1.3 // indirect
|
modernc.org/opt v0.1.3 // indirect
|
||||||
modernc.org/sqlite v1.20.4 // indirect
|
|
||||||
modernc.org/strutil v1.2.0 // indirect
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
modernc.org/token v1.1.0 // indirect
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -269,8 +269,6 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
|||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||||
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -314,6 +312,10 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
|
|||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||||
@@ -551,6 +553,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
@@ -569,6 +573,10 @@ github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsR
|
|||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
|
||||||
|
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.12/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||||
@@ -577,8 +585,6 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
|||||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
|
|
||||||
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
@@ -595,6 +601,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
|||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||||
|
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
@@ -704,6 +712,8 @@ github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
|
|||||||
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
||||||
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
|
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
@@ -716,6 +726,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
|
|||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||||
|
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||||
|
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -787,12 +799,12 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
@@ -802,8 +814,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -821,8 +833,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -870,8 +882,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -882,8 +894,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -894,8 +906,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -908,8 +920,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -950,8 +962,20 @@ lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
|||||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||||
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||||
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||||
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
@@ -960,12 +984,18 @@ modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
|||||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
|
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
|
||||||
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
|
||||||
|
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||||
|
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||||
pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
||||||
|
|||||||
@@ -436,6 +436,12 @@ type GetFeedsOptions struct {
|
|||||||
DontCount bool // do counting in GetFeeds
|
DontCount bool // do counting in GetFeeds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.IncludePrivate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ActivityReadable return whether doer can read activities of user
|
// ActivityReadable return whether doer can read activities of user
|
||||||
func ActivityReadable(user, doer *user_model.User) bool {
|
func ActivityReadable(user, doer *user_model.User) bool {
|
||||||
return !user.KeepActivityPrivate ||
|
return !user.KeepActivityPrivate ||
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package activities
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
"code.gitea.io/gitea/models/auth"
|
"code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/models/webhook"
|
"code.gitea.io/gitea/models/webhook"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
)
|
)
|
||||||
@@ -37,6 +39,11 @@ type Statistic struct {
|
|||||||
Branches, Tags, CommitStatus int64
|
Branches, Tags, CommitStatus int64
|
||||||
IssueByLabel []IssueByLabelCount
|
IssueByLabel []IssueByLabelCount
|
||||||
IssueByRepository []IssueByRepositoryCount
|
IssueByRepository []IssueByRepositoryCount
|
||||||
|
|
||||||
|
// MokoGitea extended metrics
|
||||||
|
ActiveUsers30d int64
|
||||||
|
ActionsQueueLength int64
|
||||||
|
ActionsRunningJobs int64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,5 +138,19 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
|
|||||||
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
|
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
|
||||||
stats.Counter.Project, _ = e.Count(new(project_model.Project))
|
stats.Counter.Project, _ = e.Count(new(project_model.Project))
|
||||||
stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
|
stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
|
||||||
|
|
||||||
|
// MokoGitea extended metrics
|
||||||
|
// Active users in last 30 days (users who performed any action)
|
||||||
|
stats.Counter.ActiveUsers30d, _ = e.Where("last_login_unix > ?",
|
||||||
|
timeutil.TimeStampNow()-30*24*60*60).Count(new(user_model.User))
|
||||||
|
|
||||||
|
// Actions queue and running jobs (if actions enabled)
|
||||||
|
if setting.Actions.Enabled {
|
||||||
|
stats.Counter.ActionsQueueLength, _ = e.Where("status = ?", 1). // StatusWaiting
|
||||||
|
Count(new(actions_model.ActionRunJob))
|
||||||
|
stats.Counter.ActionsRunningJobs, _ = e.Where("status = ?", 2). // StatusRunning
|
||||||
|
Count(new(actions_model.ActionRunJob))
|
||||||
|
}
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
|
|||||||
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
|
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if opts.AWSSecretAccessKeyEncrypted != "" {
|
||||||
|
if opts.AWSSecretAccessKey, err = secret.DecryptSecret(setting.SecretKey, opts.AWSSecretAccessKeyEncrypted); err != nil {
|
||||||
|
log.Error("Unable to decrypt AWSSecretAccessKey, maybe SECRET_KEY is wrong: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &opts, nil
|
return &opts, nil
|
||||||
}
|
}
|
||||||
@@ -201,6 +206,8 @@ func FinishMigrateTask(ctx context.Context, task *Task) error {
|
|||||||
conf.AuthPasswordEncrypted = ""
|
conf.AuthPasswordEncrypted = ""
|
||||||
conf.AuthTokenEncrypted = ""
|
conf.AuthTokenEncrypted = ""
|
||||||
conf.CloneAddrEncrypted = ""
|
conf.CloneAddrEncrypted = ""
|
||||||
|
conf.AWSSecretAccessKey = ""
|
||||||
|
conf.AWSSecretAccessKeyEncrypted = ""
|
||||||
confBytes, err := json.Marshal(conf)
|
confBytes, err := json.Marshal(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content
|
|||||||
if !email.IsActivated {
|
if !email.IsActivated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if content == email.Email {
|
if strings.EqualFold(content, email.LowerEmail) {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-38
@@ -5,9 +5,8 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/subtle"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -24,6 +23,7 @@ import (
|
|||||||
|
|
||||||
uuid "github.com/google/uuid"
|
uuid "github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,10 @@ import (
|
|||||||
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
||||||
const oauth2AuthorizationCodeValidity = 10 * time.Minute
|
const oauth2AuthorizationCodeValidity = 10 * time.Minute
|
||||||
|
|
||||||
var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
|
var (
|
||||||
|
ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
|
||||||
|
ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh")
|
||||||
|
)
|
||||||
|
|
||||||
// OAuth2Application represents an OAuth2 client (RFC 6749)
|
// OAuth2Application represents an OAuth2 client (RFC 6749)
|
||||||
type OAuth2Application struct {
|
type OAuth2Application struct {
|
||||||
@@ -151,30 +154,40 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool {
|
|||||||
// https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
|
// https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1
|
||||||
contains := func(s string) bool {
|
redirectCandidates := []string{redirectURI}
|
||||||
s = strings.TrimSuffix(strings.ToLower(s), "/")
|
if !app.ConfidentialClient {
|
||||||
for _, u := range app.RedirectURIs {
|
loopbackRedirect, ok := normalizePublicClientRedirectURI(redirectURI)
|
||||||
if strings.TrimSuffix(strings.ToLower(u), "/") == s {
|
if ok {
|
||||||
|
redirectCandidates = append(redirectCandidates, loopbackRedirect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, candidate := range redirectCandidates {
|
||||||
|
normalizedCandidate := normalizeRedirectURIForComparison(candidate)
|
||||||
|
for _, registeredURI := range app.RedirectURIs {
|
||||||
|
if normalizeRedirectURIForComparison(registeredURI) == normalizedCandidate {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
if !app.ConfidentialClient {
|
|
||||||
uri, err := url.Parse(redirectURI)
|
return false
|
||||||
// ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
}
|
||||||
if err == nil && uri.Scheme == "http" && uri.Port() != "" {
|
|
||||||
ip := net.ParseIP(uri.Hostname())
|
func normalizeRedirectURIForComparison(redirectURI string) string {
|
||||||
if ip != nil && ip.IsLoopback() {
|
return strings.TrimSuffix(util.ToLowerASCII(redirectURI), "/")
|
||||||
// strip port
|
}
|
||||||
uri.Host = uri.Hostname()
|
|
||||||
if contains(uri.String()) {
|
func normalizePublicClientRedirectURI(redirectURI string) (string, bool) {
|
||||||
return true
|
parsedURI, err := url.Parse(redirectURI)
|
||||||
}
|
if err != nil || parsedURI.Scheme != "http" || parsedURI.Port() == "" {
|
||||||
}
|
return "", false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return contains(redirectURI)
|
if ip := net.ParseIP(parsedURI.Hostname()); ip == nil || !ip.IsLoopback() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
parsedURI.Host = parsedURI.Hostname()
|
||||||
|
return parsedURI.String(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base32 characters, but lowercased.
|
// Base32 characters, but lowercased.
|
||||||
@@ -424,22 +437,34 @@ func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool {
|
||||||
|
return code.CodeChallengeMethod != "" || code.CodeChallenge != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveCodeChallenge(method, verifier string) (string, bool) {
|
||||||
|
switch method {
|
||||||
|
case "S256":
|
||||||
|
return oauth2.S256ChallengeFromVerifier(verifier), true
|
||||||
|
case "plain":
|
||||||
|
return verifier, true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
|
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
|
||||||
func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
|
func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool {
|
||||||
switch code.CodeChallengeMethod {
|
if !code.requiresCodeVerifier() {
|
||||||
case "S256":
|
|
||||||
// base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6
|
|
||||||
h := sha256.Sum256([]byte(verifier))
|
|
||||||
hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:])
|
|
||||||
return hashedVerifier == code.CodeChallenge
|
|
||||||
case "plain":
|
|
||||||
return verifier == code.CodeChallenge
|
|
||||||
case "":
|
|
||||||
return true
|
return true
|
||||||
default:
|
}
|
||||||
// unsupported method -> return false
|
if verifier == "" || code.CodeChallengeMethod == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
expectedChallenge, ok := deriveCodeChallenge(code.CodeChallengeMethod, verifier)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return subtle.ConstantTimeCompare([]byte(expectedChallenge), []byte(code.CodeChallenge)) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOAuth2AuthorizationByCode returns an authorization by its code
|
// GetOAuth2AuthorizationByCode returns an authorization by its code
|
||||||
@@ -504,15 +529,18 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi
|
|||||||
|
|
||||||
// IncreaseCounter increases the counter and updates the grant
|
// IncreaseCounter increases the counter and updates the grant
|
||||||
func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
|
func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
|
||||||
_, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant))
|
affected, err := db.GetEngine(ctx).
|
||||||
|
Where("id = ?", grant.ID).
|
||||||
|
And("counter = ?", grant.Counter).
|
||||||
|
Incr("counter").
|
||||||
|
Update(new(OAuth2Grant))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID)
|
if affected == 0 {
|
||||||
if err != nil {
|
return ErrOAuth2GrantStaleCounter
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
grant.Counter = updatedGrant.Counter
|
grant.Counter++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+80
-25
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode(t *testing.T) {
|
func TestOAuth2AuthorizationCode(t *testing.T) {
|
||||||
@@ -116,6 +117,47 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) {
|
|||||||
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
|
assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
registered []string
|
||||||
|
redirectURI string
|
||||||
|
allowed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "exact-match",
|
||||||
|
registered: []string{"https://signin.example.test/callback"},
|
||||||
|
redirectURI: "https://signin.example.test/callback",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ascii-case-insensitive",
|
||||||
|
registered: []string{"https://signin.example.test/callback"},
|
||||||
|
redirectURI: "https://signIN.example.test/callback",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-ascii-not-folded",
|
||||||
|
registered: []string{"https://signin.example.test/callback"},
|
||||||
|
redirectURI: "https://signİn.example.test/callback",
|
||||||
|
allowed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "loopback-strips-port",
|
||||||
|
registered: []string{"http://127.0.0.1/callback"},
|
||||||
|
redirectURI: "http://127.0.0.1:12345/callback",
|
||||||
|
allowed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
app := &auth_model.OAuth2Application{RedirectURIs: tc.registered}
|
||||||
|
assert.Equal(t, tc.allowed, app.ContainsRedirectURI(tc.redirectURI))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
|
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
||||||
@@ -193,6 +235,16 @@ func TestOAuth2Grant_IncreaseCounter(t *testing.T) {
|
|||||||
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2})
|
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1})
|
||||||
|
stale := *grant
|
||||||
|
|
||||||
|
assert.NoError(t, grant.IncreaseCounter(t.Context()))
|
||||||
|
err := stale.IncreaseCounter(t.Context())
|
||||||
|
assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter)
|
||||||
|
}
|
||||||
|
|
||||||
func TestOAuth2Grant_ScopeContains(t *testing.T) {
|
func TestOAuth2Grant_ScopeContains(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"})
|
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"})
|
||||||
@@ -237,35 +289,38 @@ func TestRevokeOAuth2Grant(t *testing.T) {
|
|||||||
//////////////////// Authorization Code
|
//////////////////// Authorization Code
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
|
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
|
||||||
// test plain
|
s256Verifier := "s256-verifier"
|
||||||
code := &auth_model.OAuth2AuthorizationCode{
|
s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier)
|
||||||
CodeChallengeMethod: "plain",
|
missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied")
|
||||||
CodeChallenge: "test123",
|
|
||||||
}
|
|
||||||
assert.True(t, code.ValidateCodeChallenge("test123"))
|
|
||||||
assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio"))
|
|
||||||
|
|
||||||
// test S256
|
testCases := []struct {
|
||||||
code = &auth_model.OAuth2AuthorizationCode{
|
name string
|
||||||
CodeChallengeMethod: "S256",
|
method string
|
||||||
CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg",
|
challenge string
|
||||||
|
verifier string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"plain-success", "plain", "plain-secret", "plain-secret", true},
|
||||||
|
{"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false},
|
||||||
|
{"s256-success", "S256", s256Challenge, s256Verifier, true},
|
||||||
|
{"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false},
|
||||||
|
{"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false},
|
||||||
|
{"no-pkce-configured", "", "", "", true},
|
||||||
|
{"s256-missing-verifier", "S256", missingVerifierChallenge, "", false},
|
||||||
|
{"plain-missing-verifier", "plain", "plain-secret", "", false},
|
||||||
|
{"missing-method-with-challenge", "", "foierjiogerogerg", "", false},
|
||||||
|
{"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false},
|
||||||
}
|
}
|
||||||
assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt"))
|
|
||||||
assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg"))
|
|
||||||
|
|
||||||
// test unknown
|
for _, tc := range testCases {
|
||||||
code = &auth_model.OAuth2AuthorizationCode{
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
CodeChallengeMethod: "monkey",
|
code := &auth_model.OAuth2AuthorizationCode{
|
||||||
CodeChallenge: "foiwgjioriogeiogjerger",
|
CodeChallengeMethod: tc.method,
|
||||||
|
CodeChallenge: tc.challenge,
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger"))
|
|
||||||
|
|
||||||
// test no code challenge
|
|
||||||
code = &auth_model.OAuth2AuthorizationCode{
|
|
||||||
CodeChallengeMethod: "",
|
|
||||||
CodeChallenge: "foierjiogerogerg",
|
|
||||||
}
|
|
||||||
assert.True(t, code.ValidateCodeChallenge(""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
|
func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ type FindOrgOptions struct {
|
|||||||
IncludeVisibility structs.VisibleType
|
IncludeVisibility structs.VisibleType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.IncludeVisibility = structs.VisibleTypePublic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
||||||
cond := builder.Eq{"uid": userID}
|
cond := builder.Eq{"uid": userID}
|
||||||
if !includePrivate {
|
if !includePrivate {
|
||||||
|
|||||||
@@ -212,6 +212,13 @@ type SearchRepoOptions struct {
|
|||||||
OnlyShowRelevant bool
|
OnlyShowRelevant bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.Private = false
|
||||||
|
opts.AllLimited = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UserOwnedRepoCond returns user ownered repositories
|
// UserOwnedRepoCond returns user ownered repositories
|
||||||
func UserOwnedRepoCond(userID int64) builder.Cond {
|
func UserOwnedRepoCond(userID int64) builder.Cond {
|
||||||
return builder.Eq{
|
return builder.Eq{
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ type StarredReposOptions struct {
|
|||||||
IncludePrivate bool
|
IncludePrivate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.IncludePrivate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *StarredReposOptions) ToConds() builder.Cond {
|
func (opts *StarredReposOptions) ToConds() builder.Cond {
|
||||||
var cond builder.Cond = builder.Eq{
|
var cond builder.Cond = builder.Eq{
|
||||||
"star.uid": opts.StarrerID,
|
"star.uid": opts.StarrerID,
|
||||||
@@ -62,6 +68,12 @@ type WatchedReposOptions struct {
|
|||||||
IncludePrivate bool
|
IncludePrivate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.IncludePrivate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
||||||
var cond builder.Cond = builder.Eq{
|
var cond builder.Cond = builder.Eq{
|
||||||
"watch.user_id": opts.WatcherID,
|
"watch.user_id": opts.WatcherID,
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ type SearchUserOptions struct {
|
|||||||
IncludeReserved bool
|
IncludeReserved bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
|
||||||
|
if publicOnly {
|
||||||
|
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
||||||
var cond builder.Cond
|
var cond builder.Cond
|
||||||
cond = builder.In("type", opts.Types)
|
cond = builder.In("type", opts.Types)
|
||||||
|
|||||||
@@ -307,6 +307,13 @@ func (u *User) DashboardLink() string {
|
|||||||
return setting.AppSubURL + "/"
|
return setting.AppSubURL + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) SettingsLink() string {
|
||||||
|
if u.IsOrganization() {
|
||||||
|
return u.OrganisationLink() + "/settings"
|
||||||
|
}
|
||||||
|
return setting.AppSubURL + "/user/settings"
|
||||||
|
}
|
||||||
|
|
||||||
// HomeLink returns the user or organization home page link.
|
// HomeLink returns the user or organization home page link.
|
||||||
func (u *User) HomeLink() string {
|
func (u *User) HomeLink() string {
|
||||||
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
|
return setting.AppSubURL + "/" + url.PathEscape(u.Name)
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,6 +19,22 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type tagType string
|
||||||
|
|
||||||
|
// BuildSignature builds a hmac signature for the input values.
|
||||||
|
// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose.
|
||||||
|
func BuildSignature(tag tagType, vals ...string) []byte {
|
||||||
|
m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||||
|
_, _ = io.WriteString(m, string(tag))
|
||||||
|
var buf8 [8]byte
|
||||||
|
for _, v := range vals {
|
||||||
|
binary.LittleEndian.PutUint64(buf8[:], uint64(len(v)))
|
||||||
|
_, _ = m.Write(buf8[:])
|
||||||
|
_, _ = io.WriteString(m, v)
|
||||||
|
}
|
||||||
|
return m.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
// IsArtifactV4 detects whether the artifact is likely from v4.
|
// IsArtifactV4 detects whether the artifact is likely from v4.
|
||||||
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
|
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
|
||||||
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
|
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildSignature(t *testing.T) {
|
||||||
|
a := BuildSignature("v0", "x")
|
||||||
|
b := BuildSignature("v0", "x")
|
||||||
|
assert.Equal(t, a, b)
|
||||||
|
|
||||||
|
a = BuildSignature("v0", "x", "yz")
|
||||||
|
b = BuildSignature("v0", "xy", "z")
|
||||||
|
assert.NotEqual(t, a, b)
|
||||||
|
|
||||||
|
a = BuildSignature("v1", "x")
|
||||||
|
b = BuildSignature("v2", "x")
|
||||||
|
assert.NotEqual(t, a, b)
|
||||||
|
|
||||||
|
a = BuildSignature("v0", "x")
|
||||||
|
b = BuildSignature("v0x")
|
||||||
|
assert.NotEqual(t, a, b)
|
||||||
|
|
||||||
|
a = BuildSignature("v0", "", "x")
|
||||||
|
b = BuildSignature("v0", "x", "")
|
||||||
|
assert.NotEqual(t, a, b)
|
||||||
|
|
||||||
|
a = BuildSignature("v0")
|
||||||
|
b = BuildSignature("v0")
|
||||||
|
assert.Equal(t, a, b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package badge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preset colors for repo badges
|
||||||
|
const (
|
||||||
|
ColorGreen = "#4c1"
|
||||||
|
ColorYellow = "#dfb317"
|
||||||
|
ColorRed = "#e05d44"
|
||||||
|
ColorGrey = "#9f9f9f"
|
||||||
|
ColorBlue = "#007ec6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateRepoBadge creates a badge for the given repo and badge type.
|
||||||
|
func GenerateRepoBadge(ctx context.Context, repo *repo_model.Repository, badgeType string) Badge {
|
||||||
|
switch strings.ToLower(badgeType) {
|
||||||
|
case "version":
|
||||||
|
return versionBadge(ctx, repo)
|
||||||
|
case "build":
|
||||||
|
return buildBadge(ctx, repo)
|
||||||
|
case "license":
|
||||||
|
return licenseBadge(ctx, repo)
|
||||||
|
case "health":
|
||||||
|
return healthBadge(ctx, repo)
|
||||||
|
default:
|
||||||
|
return GenerateBadge("badge", "unknown", ColorGrey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionBadge(ctx context.Context, repo *repo_model.Repository) Badge {
|
||||||
|
release, err := repo_model.GetLatestReleaseByRepoID(ctx, repo.ID)
|
||||||
|
if err != nil || release == nil {
|
||||||
|
return GenerateBadge("version", "none", ColorGrey)
|
||||||
|
}
|
||||||
|
return GenerateBadge("version", release.TagName, ColorBlue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBadge(ctx context.Context, repo *repo_model.Repository) Badge {
|
||||||
|
status, err := git_model.GetLatestCommitStatus(ctx, repo.ID, repo.DefaultBranch, db.ListOptions{PageSize: 1})
|
||||||
|
if err != nil || len(status) == 0 {
|
||||||
|
return GenerateBadge("build", "unknown", ColorGrey)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status[0].State.String() {
|
||||||
|
case "success":
|
||||||
|
return GenerateBadge("build", "passing", ColorGreen)
|
||||||
|
case "failure", "error":
|
||||||
|
return GenerateBadge("build", "failing", ColorRed)
|
||||||
|
case "pending":
|
||||||
|
return GenerateBadge("build", "pending", ColorYellow)
|
||||||
|
default:
|
||||||
|
return GenerateBadge("build", status[0].State.String(), ColorGrey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func licenseBadge(ctx context.Context, repo *repo_model.Repository) Badge {
|
||||||
|
licenses, err := repo_model.GetRepoLicenses(ctx, repo)
|
||||||
|
if err != nil || len(licenses) == 0 {
|
||||||
|
return GenerateBadge("license", "none", ColorGrey)
|
||||||
|
}
|
||||||
|
return GenerateBadge("license", licenses.StringList()[0], ColorBlue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func healthBadge(ctx context.Context, repo *repo_model.Repository) Badge {
|
||||||
|
score := 0
|
||||||
|
licenses, _ := repo_model.GetRepoLicenses(ctx, repo)
|
||||||
|
if len(licenses) > 0 {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
if repo.Description != "" {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
if len(repo.Topics) > 0 {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case score >= 3:
|
||||||
|
return GenerateBadge("health", "healthy", ColorGreen)
|
||||||
|
case score >= 2:
|
||||||
|
return GenerateBadge("health", "fair", ColorYellow)
|
||||||
|
default:
|
||||||
|
return GenerateBadge("health", "needs work", ColorRed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatRepoBadgeSVG returns the badge as SVG HTML for the given repo badge type.
|
||||||
|
func FormatRepoBadgeSVG(ctx context.Context, repo *repo_model.Repository, badgeType, style string) (string, error) {
|
||||||
|
b := GenerateRepoBadge(ctx, repo, badgeType)
|
||||||
|
switch style {
|
||||||
|
case "flat-square":
|
||||||
|
return b.RenderFlatSquare(), nil
|
||||||
|
default:
|
||||||
|
return b.RenderFlat(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFlat returns the badge as flat-style SVG.
|
||||||
|
func (b Badge) RenderFlat() string {
|
||||||
|
return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
|
||||||
|
<rect width="%d" height="20" fill="#555"/><rect x="%d" width="%d" height="20" fill="%s"/>
|
||||||
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
|
||||||
|
<text x="%d" y="14">%s</text><text x="%d" y="14">%s</text>
|
||||||
|
</g></svg>`,
|
||||||
|
b.Label.Width()+b.Message.Width(), b.Label.Width(), b.Label.Width(), b.Message.Width(), b.Color,
|
||||||
|
b.Label.X(), b.Label.Text(), b.Message.X(), b.Message.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderFlatSquare returns the badge as flat-square-style SVG.
|
||||||
|
func (b Badge) RenderFlatSquare() string {
|
||||||
|
return b.RenderFlat() // same for now
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
|||||||
".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML
|
".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML
|
||||||
".m": "Objective-C", // Matlab, Mathematica, Mason
|
".m": "Objective-C", // Matlab, Mathematica, Mason
|
||||||
".mc": "Mason", // MonkeyC
|
".mc": "Mason", // MonkeyC
|
||||||
".mod": "AMPL", // Modula-2
|
|
||||||
".network": "SYSTEMD", // INI
|
".network": "SYSTEMD", // INI
|
||||||
".php": "PHP", // PHTML
|
".php": "PHP", // PHTML
|
||||||
".php3": "PHP", // PHTML
|
".php3": "PHP", // PHTML
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ type Collector struct {
|
|||||||
Users *prometheus.Desc
|
Users *prometheus.Desc
|
||||||
Watches *prometheus.Desc
|
Watches *prometheus.Desc
|
||||||
Webhooks *prometheus.Desc
|
Webhooks *prometheus.Desc
|
||||||
|
|
||||||
|
// MokoGitea extended metrics
|
||||||
|
ActiveUsers30d *prometheus.Desc
|
||||||
|
ActionsQueueLength *prometheus.Desc
|
||||||
|
ActionsRunningJobs *prometheus.Desc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCollector returns a new Collector with all prometheus.Desc initialized
|
// NewCollector returns a new Collector with all prometheus.Desc initialized
|
||||||
@@ -196,6 +201,21 @@ func NewCollector() Collector {
|
|||||||
"Number of Webhooks",
|
"Number of Webhooks",
|
||||||
nil, nil,
|
nil, nil,
|
||||||
),
|
),
|
||||||
|
ActiveUsers30d: prometheus.NewDesc(
|
||||||
|
namespace+"active_users_30d",
|
||||||
|
"Number of active users in the last 30 days",
|
||||||
|
nil, nil,
|
||||||
|
),
|
||||||
|
ActionsQueueLength: prometheus.NewDesc(
|
||||||
|
namespace+"actions_queue_length",
|
||||||
|
"Number of actions jobs waiting to run",
|
||||||
|
nil, nil,
|
||||||
|
),
|
||||||
|
ActionsRunningJobs: prometheus.NewDesc(
|
||||||
|
namespace+"actions_running_jobs",
|
||||||
|
"Number of actions jobs currently running",
|
||||||
|
nil, nil,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +249,9 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
|
|||||||
ch <- c.Users
|
ch <- c.Users
|
||||||
ch <- c.Watches
|
ch <- c.Watches
|
||||||
ch <- c.Webhooks
|
ch <- c.Webhooks
|
||||||
|
ch <- c.ActiveUsers30d
|
||||||
|
ch <- c.ActionsQueueLength
|
||||||
|
ch <- c.ActionsRunningJobs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect returns the metrics with values
|
// Collect returns the metrics with values
|
||||||
@@ -392,4 +415,21 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
|
|||||||
prometheus.GaugeValue,
|
prometheus.GaugeValue,
|
||||||
float64(stats.Counter.Webhook),
|
float64(stats.Counter.Webhook),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MokoGitea extended metrics
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.ActiveUsers30d,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(stats.Counter.ActiveUsers30d),
|
||||||
|
)
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.ActionsQueueLength,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(stats.Counter.ActionsQueueLength),
|
||||||
|
)
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.ActionsRunningJobs,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(stats.Counter.ActionsRunningJobs),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,5 +40,7 @@ type MigrateOptions struct {
|
|||||||
MirrorInterval string `json:"mirror_interval"`
|
MirrorInterval string `json:"mirror_interval"`
|
||||||
|
|
||||||
AWSAccessKeyID string
|
AWSAccessKeyID string
|
||||||
AWSSecretAccessKey string
|
AWSSecretAccessKey string `json:",omitempty"`
|
||||||
|
|
||||||
|
AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const IncomingEmailTokenPlaceholder = "%{token}"
|
||||||
|
|
||||||
var IncomingEmail = struct {
|
var IncomingEmail = struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ReplyToAddress string
|
ReplyToAddress string
|
||||||
TokenPlaceholder string `ini:"-"`
|
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
UseTLS bool `ini:"USE_TLS"`
|
UseTLS bool `ini:"USE_TLS"`
|
||||||
@@ -28,7 +29,6 @@ var IncomingEmail = struct {
|
|||||||
}{
|
}{
|
||||||
Mailbox: "INBOX",
|
Mailbox: "INBOX",
|
||||||
DeleteHandledMessage: true,
|
DeleteHandledMessage: true,
|
||||||
TokenPlaceholder: "%{token}",
|
|
||||||
MaximumMessageSize: 10485760,
|
MaximumMessageSize: 10485760,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,19 +54,10 @@ func checkReplyToAddress() error {
|
|||||||
return errors.New("name must not be set")
|
return errors.New("name must not be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder)
|
placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder)
|
||||||
switch c {
|
userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@")
|
||||||
case 0:
|
if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) {
|
||||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder)
|
||||||
case 1:
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(IncomingEmail.ReplyToAddress, "@")
|
|
||||||
if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) {
|
|
||||||
return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
// Ntfy holds ntfy push notification settings.
|
||||||
|
var Ntfy = struct {
|
||||||
|
Enabled bool
|
||||||
|
ServerURL string
|
||||||
|
DefaultTopic string
|
||||||
|
Token string
|
||||||
|
}{
|
||||||
|
Enabled: false,
|
||||||
|
ServerURL: "https://ntfy.mokoconsulting.tech",
|
||||||
|
DefaultTopic: "mokogitea",
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNtfyFrom(cfg ConfigProvider) {
|
||||||
|
sec := cfg.Section("ntfy")
|
||||||
|
Ntfy.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||||
|
Ntfy.ServerURL = sec.Key("SERVER_URL").MustString(Ntfy.ServerURL)
|
||||||
|
Ntfy.DefaultTopic = sec.Key("DEFAULT_TOPIC").MustString(Ntfy.DefaultTopic)
|
||||||
|
Ntfy.Token = sec.Key("TOKEN").String()
|
||||||
|
}
|
||||||
@@ -28,6 +28,17 @@ var (
|
|||||||
CfgProvider ConfigProvider
|
CfgProvider ConfigProvider
|
||||||
IsWindows bool
|
IsWindows bool
|
||||||
|
|
||||||
|
// UpdateChecker configuration for MokoGitea version checking
|
||||||
|
UpdateChecker = struct {
|
||||||
|
Enabled bool
|
||||||
|
Endpoint string
|
||||||
|
Channel string // stable, dev, security
|
||||||
|
}{
|
||||||
|
Enabled: true,
|
||||||
|
Endpoint: "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/raw/branch/main/updates.xml",
|
||||||
|
Channel: "stable",
|
||||||
|
}
|
||||||
|
|
||||||
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
|
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
|
||||||
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
|
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
|
||||||
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
|
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
|
||||||
@@ -158,9 +169,18 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||||||
loadMarkupFrom(cfg)
|
loadMarkupFrom(cfg)
|
||||||
loadGlobalLockFrom(cfg)
|
loadGlobalLockFrom(cfg)
|
||||||
loadOtherFrom(cfg)
|
loadOtherFrom(cfg)
|
||||||
|
loadUpdateCheckerFrom(cfg)
|
||||||
|
loadNtfyFrom(cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadUpdateCheckerFrom(cfg ConfigProvider) {
|
||||||
|
sec := cfg.Section("update_checker")
|
||||||
|
UpdateChecker.Enabled = sec.Key("ENABLED").MustBool(true)
|
||||||
|
UpdateChecker.Endpoint = sec.Key("ENDPOINT").MustString(UpdateChecker.Endpoint)
|
||||||
|
UpdateChecker.Channel = sec.Key("CHANNEL").MustString(UpdateChecker.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
func loadRunModeFrom(rootCfg ConfigProvider) {
|
func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||||
rootSec := rootCfg.Section("")
|
rootSec := rootCfg.Section("")
|
||||||
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
|
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package structs
|
||||||
|
|
||||||
|
// IssueBulkLabelsOption options for bulk label operations on issues
|
||||||
|
type IssueBulkLabelsOption struct {
|
||||||
|
// issue indexes to operate on
|
||||||
|
// required: true
|
||||||
|
Issues []int64 `json:"issues" binding:"Required"`
|
||||||
|
// Labels can be a list of integers representing label IDs
|
||||||
|
// or a list of strings representing label names
|
||||||
|
// required: true
|
||||||
|
Labels []any `json:"labels" binding:"Required"`
|
||||||
|
// action to perform: "add" (default), "remove", or "replace"
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueBulkStateOption options for bulk state changes on issues
|
||||||
|
type IssueBulkStateOption struct {
|
||||||
|
// issue indexes to operate on
|
||||||
|
// required: true
|
||||||
|
Issues []int64 `json:"issues" binding:"Required"`
|
||||||
|
// new state for the issues, "open" or "closed"
|
||||||
|
// required: true
|
||||||
|
State string `json:"state" binding:"Required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueBulkMilestoneOption options for bulk milestone assignment on issues
|
||||||
|
type IssueBulkMilestoneOption struct {
|
||||||
|
// issue indexes to operate on
|
||||||
|
// required: true
|
||||||
|
Issues []int64 `json:"issues" binding:"Required"`
|
||||||
|
// milestone id to assign, 0 to remove milestone
|
||||||
|
// required: true
|
||||||
|
MilestoneID int64 `json:"milestone_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueBulkAssigneesOption options for bulk assignee operations on issues
|
||||||
|
type IssueBulkAssigneesOption struct {
|
||||||
|
// issue indexes to operate on
|
||||||
|
// required: true
|
||||||
|
Issues []int64 `json:"issues" binding:"Required"`
|
||||||
|
// list of assignee usernames to add or set
|
||||||
|
// required: true
|
||||||
|
Assignees []string `json:"assignees" binding:"Required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueBulkResult represents the result of a bulk issue operation
|
||||||
|
// swagger:model
|
||||||
|
type IssueBulkResult struct {
|
||||||
|
// count of successfully updated issues
|
||||||
|
SuccessCount int `json:"success_count"`
|
||||||
|
// count of issues that failed to update
|
||||||
|
FailureCount int `json:"failure_count"`
|
||||||
|
// details of failures, keyed by issue index
|
||||||
|
Failures map[int64]string `json:"failures,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package updatechecker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateInfo holds the result of the latest update check.
|
||||||
|
type UpdateInfo struct {
|
||||||
|
UpdateAvailable bool
|
||||||
|
LatestVersion string
|
||||||
|
ReleaseURL string
|
||||||
|
DockerImage string
|
||||||
|
Channel string
|
||||||
|
CheckedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cachedInfo *UpdateInfo
|
||||||
|
mu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// xmlUpdates mirrors the updates.xml structure (Joomla-style).
|
||||||
|
type xmlUpdates struct {
|
||||||
|
XMLName xml.Name `xml:"updates"`
|
||||||
|
Updates []xmlUpdate `xml:"update"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlUpdate struct {
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Version string `xml:"version"`
|
||||||
|
Tags xmlTags `xml:"tags"`
|
||||||
|
InfoURL xmlInfoURL `xml:"infourl"`
|
||||||
|
Downloads xmlDownloads `xml:"downloads"`
|
||||||
|
Maintainer string `xml:"maintainer"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlTags struct {
|
||||||
|
Tag string `xml:"tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlInfoURL struct {
|
||||||
|
Title string `xml:"title,attr"`
|
||||||
|
URL string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlDownloads struct {
|
||||||
|
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlDownloadURL struct {
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
Format string `xml:"format,attr"`
|
||||||
|
URL string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckForUpdate fetches updates.xml from the configured endpoint,
|
||||||
|
// filters by the selected channel, and compares to the running version.
|
||||||
|
func CheckForUpdate() error {
|
||||||
|
if !setting.UpdateChecker.Enabled || setting.UpdateChecker.Endpoint == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := setting.UpdateChecker.Channel
|
||||||
|
if channel == "" {
|
||||||
|
channel = "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Get(setting.UpdateChecker.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update check failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("update check returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading update response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates xmlUpdates
|
||||||
|
if err := xml.Unmarshal(body, &updates); err != nil {
|
||||||
|
return fmt.Errorf("parsing updates.xml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the entry matching the selected channel
|
||||||
|
var matched *xmlUpdate
|
||||||
|
for i := range updates.Updates {
|
||||||
|
if strings.EqualFold(updates.Updates[i].Tags.Tag, channel) {
|
||||||
|
matched = &updates.Updates[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched == nil {
|
||||||
|
log.Debug("No update entry found for channel %q", channel)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion := matched.Version
|
||||||
|
currentVersion := setting.AppVer
|
||||||
|
|
||||||
|
// Extract docker image URL if available
|
||||||
|
dockerImage := ""
|
||||||
|
for _, dl := range matched.Downloads.DownloadURL {
|
||||||
|
if dl.Format == "docker" {
|
||||||
|
dockerImage = strings.TrimSpace(dl.URL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &UpdateInfo{
|
||||||
|
LatestVersion: latestVersion,
|
||||||
|
ReleaseURL: strings.TrimSpace(matched.InfoURL.URL),
|
||||||
|
DockerImage: dockerImage,
|
||||||
|
Channel: channel,
|
||||||
|
CheckedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is available if the latest version string is not a prefix of the current version.
|
||||||
|
// e.g., current "1.26.1+305-gabcdef" does not start with "04.00.00"
|
||||||
|
// This handles both moko semver and git-describe suffixed versions.
|
||||||
|
info.UpdateAvailable = latestVersion != "" && !strings.Contains(currentVersion, latestVersion)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
cachedInfo = info
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if info.UpdateAvailable {
|
||||||
|
log.Info("MokoGitea update available: %s [%s] (current: %s)", latestVersion, channel, currentVersion)
|
||||||
|
} else {
|
||||||
|
log.Debug("MokoGitea is up to date: %s [%s]", currentVersion, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUpdateInfo returns the cached update check result.
|
||||||
|
func GetUpdateInfo() *UpdateInfo {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
if cachedInfo == nil {
|
||||||
|
return &UpdateInfo{}
|
||||||
|
}
|
||||||
|
return cachedInfo
|
||||||
|
}
|
||||||
+2
@@ -617,6 +617,7 @@
|
|||||||
"__github/workflows__": "folder-gh-workflows",
|
"__github/workflows__": "folder-gh-workflows",
|
||||||
"gitea/workflows": "folder-gitea-workflows",
|
"gitea/workflows": "folder-gitea-workflows",
|
||||||
".gitea/workflows": "folder-gitea-workflows",
|
".gitea/workflows": "folder-gitea-workflows",
|
||||||
|
".mokogitea/workflows": "folder-gitea-workflows",
|
||||||
"_gitea/workflows": "folder-gitea-workflows",
|
"_gitea/workflows": "folder-gitea-workflows",
|
||||||
"-gitea/workflows": "folder-gitea-workflows",
|
"-gitea/workflows": "folder-gitea-workflows",
|
||||||
"__gitea/workflows__": "folder-gitea-workflows",
|
"__gitea/workflows__": "folder-gitea-workflows",
|
||||||
@@ -5237,6 +5238,7 @@
|
|||||||
"__github/workflows__": "folder-gh-workflows-open",
|
"__github/workflows__": "folder-gh-workflows-open",
|
||||||
"gitea/workflows": "folder-gitea-workflows-open",
|
"gitea/workflows": "folder-gitea-workflows-open",
|
||||||
".gitea/workflows": "folder-gitea-workflows-open",
|
".gitea/workflows": "folder-gitea-workflows-open",
|
||||||
|
".mokogitea/workflows": "folder-gitea-workflows-open",
|
||||||
"_gitea/workflows": "folder-gitea-workflows-open",
|
"_gitea/workflows": "folder-gitea-workflows-open",
|
||||||
"-gitea/workflows": "folder-gitea-workflows-open",
|
"-gitea/workflows": "folder-gitea-workflows-open",
|
||||||
"__gitea/workflows__": "folder-gitea-workflows-open",
|
"__gitea/workflows__": "folder-gitea-workflows-open",
|
||||||
|
|||||||
@@ -3626,7 +3626,13 @@
|
|||||||
"packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.",
|
"packages.terraform.delete.latest": "The latest version of a Terraform state cannot be deleted.",
|
||||||
"packages.vagrant.install": "To add a Vagrant box, run the following command:",
|
"packages.vagrant.install": "To add a Vagrant box, run the following command:",
|
||||||
"packages.settings.link": "Link this package to a repository",
|
"packages.settings.link": "Link this package to a repository",
|
||||||
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list. Only repositories under the same owner can be linked. Leaving the field empty will remove the link.",
|
"packages.settings.link.description": "If you link a package with a repository, the package will appear in the repository's package list.",
|
||||||
|
"packages.settings.link.notice1": "Only repositories under the same owner can be linked.",
|
||||||
|
"packages.settings.link.notice2": "Linking a repository does not change the package visibility.",
|
||||||
|
"packages.settings.link.notice3": "Leaving the field empty will remove the link.",
|
||||||
|
"packages.settings.visibility": "Package visibility",
|
||||||
|
"packages.settings.visibility.inherit": "Package visibility is inherited from the owner and cannot be changed independently here. To change it, update the visibility settings of the user or organization that owns this package.",
|
||||||
|
"packages.settings.visibility.button": "Change owner visibility",
|
||||||
"packages.settings.link.select": "Select Repository",
|
"packages.settings.link.select": "Select Repository",
|
||||||
"packages.settings.link.button": "Update Repository Link",
|
"packages.settings.link.button": "Update Repository Link",
|
||||||
"packages.settings.link.success": "Repository link was successfully updated.",
|
"packages.settings.link.success": "Repository link was successfully updated.",
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@
|
|||||||
"jquery": "4.0.0",
|
"jquery": "4.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"katex": "0.16.44",
|
"katex": "0.16.44",
|
||||||
"mermaid": "11.14.0",
|
"mermaid": "11.15.0",
|
||||||
"online-3d-viewer": "0.18.0",
|
"online-3d-viewer": "0.18.0",
|
||||||
"pdfobject": "2.3.1",
|
"pdfobject": "2.3.1",
|
||||||
"perfect-debounce": "2.1.0",
|
"perfect-debounce": "2.1.0",
|
||||||
|
|||||||
Generated
+532
-116
File diff suppressed because it is too large
Load Diff
@@ -162,13 +162,7 @@ func ArtifactsV4Routes(prefix string) *web.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
|
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
|
||||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
return actions.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
|
||||||
mac.Write([]byte(endpoint))
|
|
||||||
mac.Write([]byte(expires))
|
|
||||||
mac.Write([]byte(artifactName))
|
|
||||||
_, _ = fmt.Fprint(mac, taskID)
|
|
||||||
_, _ = fmt.Fprint(mac, artifactID)
|
|
||||||
return mac.Sum(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
|
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
packages_model "code.gitea.io/gitea/models/packages"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceIndexResponse contains registry endpoints
|
// ServiceIndexResponse contains registry endpoints
|
||||||
@@ -91,7 +94,7 @@ type Source struct {
|
|||||||
Reference string `json:"reference"`
|
Reference string `json:"reference"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||||
versions := make([]*PackageVersionMetadata, 0, len(pds))
|
versions := make([]*PackageVersionMetadata, 0, len(pds))
|
||||||
|
|
||||||
for _, pd := range pds {
|
for _, pd := range pds {
|
||||||
@@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if pd.Repository != nil {
|
if pd.Repository != nil {
|
||||||
pkg.Source = Source{
|
permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer)
|
||||||
URL: pd.Repository.HTMLURL(),
|
if err != nil {
|
||||||
Type: "git",
|
log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err)
|
||||||
Reference: pd.Version.Version,
|
} else if permission.HasAnyUnitAccessOrPublicAccess() {
|
||||||
|
pkg.Source = Source{
|
||||||
|
URL: pd.Repository.HTMLURL(),
|
||||||
|
Type: "git",
|
||||||
|
Reference: pd.Version.Version,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp := createPackageMetadataResponse(
|
resp := createPackageMetadataResponse(
|
||||||
|
ctx,
|
||||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
|
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
|
||||||
pds,
|
pds,
|
||||||
)
|
)
|
||||||
|
|||||||
+92
-61
@@ -212,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorNotFound()
|
ctx.APIErrorNotFound()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ctx.TokenCanAccessRepo(repo) {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// public Only permission check
|
for _, category := range requiredScopeCategories {
|
||||||
switch {
|
switch category {
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
|
case auth_model.AccessTokenScopeCategoryRepository:
|
||||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
|
case auth_model.AccessTokenScopeCategoryIssue:
|
||||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
|
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||||
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
|
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
|
||||||
return
|
if orgPrivate || userOrgPrivate {
|
||||||
}
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||||
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
return
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
}
|
||||||
return
|
case auth_model.AccessTokenScopeCategoryUser:
|
||||||
}
|
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
|
||||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
return
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
|
}
|
||||||
return
|
case auth_model.AccessTokenScopeCategoryActivityPub:
|
||||||
}
|
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
||||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
return
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
}
|
||||||
return
|
case auth_model.AccessTokenScopeCategoryNotification:
|
||||||
}
|
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
||||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
return
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
}
|
||||||
return
|
case auth_model.AccessTokenScopeCategoryPackage:
|
||||||
}
|
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
|
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
return
|
||||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rejectPublicOnly() func(ctx *context.APIContext) {
|
||||||
|
return func(ctx *context.APIContext) {
|
||||||
|
if !ctx.PublicOnly {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contextAuthenticatedUser() func(ctx *context.APIContext) {
|
||||||
|
return func(ctx *context.APIContext) {
|
||||||
|
ctx.ContextUser = ctx.Doer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if a token is being used for auth, we check that it contains the required scope
|
// if a token is being used for auth, we check that it contains the required scope
|
||||||
// if a token is not being used, reqToken will enforce other sign in methods
|
// if a token is not being used, reqToken will enforce other sign in methods
|
||||||
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
||||||
@@ -957,6 +977,8 @@ func Routes() *web.Router {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Notifications (requires 'notifications' scope)
|
// Notifications (requires 'notifications' scope)
|
||||||
|
// The notifications API is not available for public-only tokens because a user's notifications mix
|
||||||
|
// public and private repository events in the same mailbox.
|
||||||
m.Group("/notifications", func() {
|
m.Group("/notifications", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(reqToken(), notify.ListNotifications).
|
Get(reqToken(), notify.ListNotifications).
|
||||||
@@ -965,7 +987,7 @@ func Routes() *web.Router {
|
|||||||
m.Combo("/threads/{id}").
|
m.Combo("/threads/{id}").
|
||||||
Get(reqToken(), notify.GetThread).
|
Get(reqToken(), notify.GetThread).
|
||||||
Patch(reqToken(), notify.ReadThread)
|
Patch(reqToken(), notify.ReadThread)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
|
||||||
|
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
m.Group("/users", func() {
|
m.Group("/users", func() {
|
||||||
@@ -1013,8 +1035,9 @@ func Routes() *web.Router {
|
|||||||
m.Group("/settings", func() {
|
m.Group("/settings", func() {
|
||||||
m.Get("", user.GetUserSettings)
|
m.Get("", user.GetUserSettings)
|
||||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||||
}, reqToken())
|
}, rejectPublicOnly())
|
||||||
m.Combo("/emails").
|
// Email addresses are always private account data.
|
||||||
|
m.Combo("/emails", rejectPublicOnly()).
|
||||||
Get(user.ListEmails).
|
Get(user.ListEmails).
|
||||||
Post(bind(api.CreateEmailOption{}), user.AddEmail).
|
Post(bind(api.CreateEmailOption{}), user.AddEmail).
|
||||||
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
|
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
|
||||||
@@ -1046,7 +1069,7 @@ func Routes() *web.Router {
|
|||||||
|
|
||||||
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
|
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
|
||||||
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
|
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
|
|
||||||
m.Get("/followers", user.ListMyFollowers)
|
m.Get("/followers", user.ListMyFollowers)
|
||||||
m.Group("/following", func() {
|
m.Group("/following", func() {
|
||||||
@@ -1064,7 +1087,7 @@ func Routes() *web.Router {
|
|||||||
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
|
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
|
||||||
m.Combo("/{id}").Get(user.GetPublicKey).
|
m.Combo("/{id}").Get(user.GetPublicKey).
|
||||||
Delete(user.DeletePublicKey)
|
Delete(user.DeletePublicKey)
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
|
|
||||||
// (admin:application scope)
|
// (admin:application scope)
|
||||||
m.Group("/applications", func() {
|
m.Group("/applications", func() {
|
||||||
@@ -1075,7 +1098,7 @@ func Routes() *web.Router {
|
|||||||
Delete(user.DeleteOauth2Application).
|
Delete(user.DeleteOauth2Application).
|
||||||
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
|
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
|
||||||
Get(user.GetOauth2Application)
|
Get(user.GetOauth2Application)
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
|
|
||||||
// (admin:gpg_key scope)
|
// (admin:gpg_key scope)
|
||||||
m.Group("/gpg_keys", func() {
|
m.Group("/gpg_keys", func() {
|
||||||
@@ -1083,13 +1106,13 @@ func Routes() *web.Router {
|
|||||||
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
|
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
|
||||||
m.Combo("/{id}").Get(user.GetGPGKey).
|
m.Combo("/{id}").Get(user.GetGPGKey).
|
||||||
Delete(user.DeleteGPGKey)
|
Delete(user.DeleteGPGKey)
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
m.Get("/gpg_key_token", user.GetVerificationToken)
|
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
|
||||||
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
|
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
|
||||||
|
|
||||||
// (repo scope)
|
// (repo scope)
|
||||||
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
|
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
|
||||||
Post(bind(api.CreateRepoOption{}), repo.Create)
|
Post(rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.Create)
|
||||||
|
|
||||||
// (repo scope)
|
// (repo scope)
|
||||||
m.Group("/starred", func() {
|
m.Group("/starred", func() {
|
||||||
@@ -1100,22 +1123,22 @@ func Routes() *web.Router {
|
|||||||
m.Delete("", user.Unstar)
|
m.Delete("", user.Unstar)
|
||||||
}, repoAssignment(), checkTokenPublicOnly())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||||
m.Get("/times", repo.ListMyTrackedTimes)
|
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
|
||||||
m.Get("/stopwatches", repo.GetStopwatches)
|
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
|
||||||
m.Get("/subscriptions", user.GetMyWatchedRepos)
|
m.Get("/subscriptions", user.GetMyWatchedRepos)
|
||||||
m.Get("/teams", org.ListUserTeams)
|
m.Get("/teams", rejectPublicOnly(), org.ListUserTeams)
|
||||||
m.Group("/hooks", func() {
|
m.Group("/hooks", func() {
|
||||||
m.Combo("").Get(user.ListHooks).
|
m.Combo("").Get(user.ListHooks).
|
||||||
Post(bind(api.CreateHookOption{}), user.CreateHook)
|
Post(bind(api.CreateHookOption{}), user.CreateHook)
|
||||||
m.Combo("/{id}").Get(user.GetHook).
|
m.Combo("/{id}").Get(user.GetHook).
|
||||||
Patch(bind(api.EditHookOption{}), user.EditHook).
|
Patch(bind(api.EditHookOption{}), user.EditHook).
|
||||||
Delete(user.DeleteHook)
|
Delete(user.DeleteHook)
|
||||||
}, reqWebhooksEnabled())
|
}, reqWebhooksEnabled(), rejectPublicOnly())
|
||||||
|
|
||||||
m.Group("/avatar", func() {
|
m.Group("/avatar", func() {
|
||||||
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
|
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
|
||||||
m.Delete("", user.DeleteAvatar)
|
m.Delete("", user.DeleteAvatar)
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
|
|
||||||
m.Group("/blocks", func() {
|
m.Group("/blocks", func() {
|
||||||
m.Get("", user.ListBlocks)
|
m.Get("", user.ListBlocks)
|
||||||
@@ -1124,8 +1147,8 @@ func Routes() *web.Router {
|
|||||||
m.Put("", user.BlockUser)
|
m.Put("", user.BlockUser)
|
||||||
m.Delete("", user.UnblockUser)
|
m.Delete("", user.UnblockUser)
|
||||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||||
})
|
}, rejectPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
|
||||||
|
|
||||||
// Repositories (requires repo scope, org scope)
|
// Repositories (requires repo scope, org scope)
|
||||||
m.Post("/org/{org}/repos",
|
m.Post("/org/{org}/repos",
|
||||||
@@ -1430,9 +1453,11 @@ func Routes() *web.Router {
|
|||||||
Delete(reqToken(), repo.DeleteTopic)
|
Delete(reqToken(), repo.DeleteTopic)
|
||||||
}, reqAdmin())
|
}, reqAdmin())
|
||||||
}, reqAnyRepoReader())
|
}, reqAnyRepoReader())
|
||||||
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
// MokoGitea badge engine
|
||||||
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
|
m.Get("/badge/{type}.svg", repo.GetRepoBadge)
|
||||||
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||||
|
m.Get("/issue_config", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueConfig)
|
||||||
|
m.Get("/issue_config/validate", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.ValidateIssueConfig)
|
||||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||||
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
|
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
|
||||||
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
|
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
|
||||||
@@ -1468,6 +1493,12 @@ func Routes() *web.Router {
|
|||||||
m.Combo("").Get(repo.ListIssues).
|
m.Combo("").Get(repo.ListIssues).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||||
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||||
|
m.Group("/bulk", func() {
|
||||||
|
m.Post("/labels", bind(api.IssueBulkLabelsOption{}), repo.BulkSetIssueLabels)
|
||||||
|
m.Post("/state", bind(api.IssueBulkStateOption{}), repo.BulkSetIssueState)
|
||||||
|
m.Post("/milestone", bind(api.IssueBulkMilestoneOption{}), repo.BulkSetIssueMilestone)
|
||||||
|
m.Post("/assignees", bind(api.IssueBulkAssigneesOption{}), repo.BulkSetIssueAssignees)
|
||||||
|
}, reqToken(), mustNotBeArchived)
|
||||||
m.Group("/comments", func() {
|
m.Group("/comments", func() {
|
||||||
m.Get("", repo.ListRepoIssueComments)
|
m.Get("", repo.ListRepoIssueComments)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
@@ -1640,7 +1671,7 @@ func Routes() *web.Router {
|
|||||||
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
||||||
|
|
||||||
// Organizations
|
// Organizations
|
||||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
|
||||||
m.Group("/users/{username}/orgs", func() {
|
m.Group("/users/{username}/orgs", func() {
|
||||||
m.Get("", reqToken(), org.ListUserOrgs)
|
m.Get("", reqToken(), org.ListUserOrgs)
|
||||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
|||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
|
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -199,7 +200,7 @@ func GetAll(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/OrganizationList"
|
// "$ref": "#/responses/OrganizationList"
|
||||||
|
|
||||||
vMode := []api.VisibleType{api.VisibleTypePublic}
|
vMode := []api.VisibleType{api.VisibleTypePublic}
|
||||||
if ctx.IsSigned && !ctx.PublicOnly {
|
if ctx.IsSigned {
|
||||||
vMode = append(vMode, api.VisibleTypeLimited)
|
vMode = append(vMode, api.VisibleTypeLimited)
|
||||||
if ctx.Doer.IsAdmin {
|
if ctx.Doer.IsAdmin {
|
||||||
vMode = append(vMode, api.VisibleTypePrivate)
|
vMode = append(vMode, api.VisibleTypePrivate)
|
||||||
@@ -208,13 +209,16 @@ func GetAll(ctx *context.APIContext) {
|
|||||||
|
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
|
|
||||||
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
searchOpts := user_model.SearchUserOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||||
OrderBy: db.SearchOrderByAlphabetically,
|
OrderBy: db.SearchOrderByAlphabetically,
|
||||||
Visible: vMode,
|
Visible: vMode,
|
||||||
})
|
}
|
||||||
|
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
|
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -494,6 +498,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
|||||||
Date: ctx.FormString("date"),
|
Date: ctx.FormString("date"),
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package repo
|
|||||||
import (
|
import (
|
||||||
go_context "context"
|
go_context "context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -24,7 +23,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/actions"
|
"code.gitea.io/gitea/modules/actions"
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@@ -1939,11 +1937,7 @@ func DeleteArtifact(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildSignature(endp string, expires, artifactID int64) []byte {
|
func buildSignature(endp string, expires, artifactID int64) []byte {
|
||||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
|
||||||
mac.Write([]byte(endp))
|
|
||||||
fmt.Fprint(mac, expires)
|
|
||||||
fmt.Fprint(mac, artifactID)
|
|
||||||
return mac.Sum(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
|
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/badge"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRepoBadge returns an SVG badge for the repository.
|
||||||
|
func GetRepoBadge(ctx *context.APIContext) {
|
||||||
|
badgeType := ctx.PathParam("type")
|
||||||
|
style := ctx.FormString("style")
|
||||||
|
|
||||||
|
svg, err := badge.FormatRepoBadgeSVG(ctx, ctx.Repo.Repository, badgeType, style)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(fmt.Errorf("rendering badge: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Resp.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
ctx.Resp.Header().Set("Cache-Control", "public, max-age=300")
|
||||||
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = ctx.Resp.Write([]byte(svg))
|
||||||
|
}
|
||||||
@@ -47,9 +47,10 @@ func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPubl
|
|||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
}
|
}
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
opts.Private = !ctx.PublicOnly
|
opts.Private = true
|
||||||
opts.AllLimited = true
|
opts.AllLimited = true
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
if ctx.FormString("owner") != "" {
|
if ctx.FormString("owner") != "" {
|
||||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
// Copyright 2026 The MokoGitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BulkSetIssueLabels adds, removes, or replaces labels on multiple issues
|
||||||
|
func BulkSetIssueLabels(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/issues/bulk/labels issue issueBulkSetLabels
|
||||||
|
// ---
|
||||||
|
// summary: Add, remove, or replace labels on multiple issues
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueBulkLabelsOption"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/IssueBulkResult"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.IssueBulkLabelsOption)
|
||||||
|
if len(form.Issues) == 0 {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, errors.New("issues list must not be empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := form.Action
|
||||||
|
if action == "" {
|
||||||
|
action = "add"
|
||||||
|
}
|
||||||
|
if action != "add" && action != "remove" && action != "replace" {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("invalid action %q: must be add, remove, or replace", action))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := resolveBulkLabels(ctx, form.Labels)
|
||||||
|
if err != nil {
|
||||||
|
return // error already written to ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
result := api.IssueBulkResult{Failures: make(map[int64]string)}
|
||||||
|
|
||||||
|
for _, index := range form.Issues {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrIssueNotExist(err) {
|
||||||
|
result.Failures[index] = "issue not found"
|
||||||
|
} else {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
}
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
result.Failures[index] = "no write permission"
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "add":
|
||||||
|
err = issue_service.AddLabels(ctx, issue, ctx.Doer, labels)
|
||||||
|
case "remove":
|
||||||
|
for _, label := range labels {
|
||||||
|
if err = issue_service.RemoveLabel(ctx, issue, ctx.Doer, label); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "replace":
|
||||||
|
err = issue_service.ReplaceLabels(ctx, issue, ctx.Doer, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.SuccessCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkSetIssueState closes or reopens multiple issues
|
||||||
|
func BulkSetIssueState(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/issues/bulk/state issue issueBulkSetState
|
||||||
|
// ---
|
||||||
|
// summary: Close or reopen multiple issues
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueBulkStateOption"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/IssueBulkResult"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.IssueBulkStateOption)
|
||||||
|
if len(form.Issues) == 0 {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, errors.New("issues list must not be empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if form.State != "open" && form.State != "closed" {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("invalid state %q: must be open or closed", form.State))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := api.IssueBulkResult{Failures: make(map[int64]string)}
|
||||||
|
|
||||||
|
for _, index := range form.Issues {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrIssueNotExist(err) {
|
||||||
|
result.Failures[index] = "issue not found"
|
||||||
|
} else {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
}
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
result.Failures[index] = "no write permission"
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.State == "closed" && !issue.IsClosed {
|
||||||
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if form.State == "open" && issue.IsClosed {
|
||||||
|
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.SuccessCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkSetIssueMilestone assigns a milestone to multiple issues
|
||||||
|
func BulkSetIssueMilestone(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/issues/bulk/milestone issue issueBulkSetMilestone
|
||||||
|
// ---
|
||||||
|
// summary: Assign a milestone to multiple issues
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueBulkMilestoneOption"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/IssueBulkResult"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.IssueBulkMilestoneOption)
|
||||||
|
if len(form.Issues) == 0 {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, errors.New("issues list must not be empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate milestone exists if non-zero
|
||||||
|
if form.MilestoneID > 0 {
|
||||||
|
has, err := issues_model.HasMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.MilestoneID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
ctx.APIErrorNotFound("milestone not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := api.IssueBulkResult{Failures: make(map[int64]string)}
|
||||||
|
|
||||||
|
for _, index := range form.Issues {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrIssueNotExist(err) {
|
||||||
|
result.Failures[index] = "issue not found"
|
||||||
|
} else {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
}
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
result.Failures[index] = "no write permission"
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMilestoneID := issue.MilestoneID
|
||||||
|
issue.MilestoneID = form.MilestoneID
|
||||||
|
|
||||||
|
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.SuccessCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkSetIssueAssignees sets assignees on multiple issues
|
||||||
|
func BulkSetIssueAssignees(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/issues/bulk/assignees issue issueBulkSetAssignees
|
||||||
|
// ---
|
||||||
|
// summary: Set assignees on multiple issues
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/IssueBulkAssigneesOption"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/IssueBulkResult"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*api.IssueBulkAssigneesOption)
|
||||||
|
if len(form.Issues) == 0 {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, errors.New("issues list must not be empty"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := api.IssueBulkResult{Failures: make(map[int64]string)}
|
||||||
|
|
||||||
|
for _, index := range form.Issues {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrIssueNotExist(err) {
|
||||||
|
result.Failures[index] = "issue not found"
|
||||||
|
} else {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
}
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
|
result.Failures[index] = "no write permission"
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.LoadAssignees(ctx); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue_service.UpdateAssignees(ctx, issue, "", form.Assignees, ctx.Doer); err != nil {
|
||||||
|
result.Failures[index] = err.Error()
|
||||||
|
result.FailureCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.SuccessCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveBulkLabels resolves label IDs or names into label models
|
||||||
|
func resolveBulkLabels(ctx *context.APIContext, rawLabels []any) ([]*issues_model.Label, error) {
|
||||||
|
var (
|
||||||
|
labelIDs []int64
|
||||||
|
labelNames []string
|
||||||
|
)
|
||||||
|
for _, label := range rawLabels {
|
||||||
|
rv := reflect.ValueOf(label)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Float64:
|
||||||
|
labelIDs = append(labelIDs, int64(rv.Float()))
|
||||||
|
case reflect.String:
|
||||||
|
labelNames = append(labelNames, rv.String())
|
||||||
|
default:
|
||||||
|
ctx.APIError(http.StatusBadRequest, errors.New("a label must be an integer or a string"))
|
||||||
|
return nil, errors.New("invalid label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
||||||
|
ctx.APIError(http.StatusBadRequest, errors.New("labels should be an array of strings or integers, not both"))
|
||||||
|
return nil, errors.New("invalid labels")
|
||||||
|
}
|
||||||
|
if len(labelNames) > 0 {
|
||||||
|
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
labelIDs = append(labelIDs, repoLabelIDs...)
|
||||||
|
if ctx.Repo.Owner.IsOrganization() {
|
||||||
|
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
@@ -134,9 +134,6 @@ func Search(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/validationError"
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
|
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
|
||||||
if ctx.PublicOnly {
|
|
||||||
private = false
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := repo_model.SearchRepoOptions{
|
opts := repo_model.SearchRepoOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
@@ -152,6 +149,7 @@ func Search(ctx *context.APIContext) {
|
|||||||
StarredByID: ctx.FormInt64("starredBy"),
|
StarredByID: ctx.FormInt64("starredBy"),
|
||||||
IncludeDescription: ctx.FormBool("includeDesc"),
|
IncludeDescription: ctx.FormBool("includeDesc"),
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
if ctx.FormString("template") != "" {
|
if ctx.FormString("template") != "" {
|
||||||
opts.Template = optional.Some(ctx.FormBool("template"))
|
opts.Template = optional.Some(ctx.FormBool("template"))
|
||||||
@@ -573,6 +571,10 @@ func GetByID(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ctx.TokenCanAccessRepo(repo) {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1321,6 +1323,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
|
|||||||
Date: ctx.FormString("date"),
|
Date: ctx.FormString("date"),
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -98,6 +98,13 @@ type swaggerIssueTemplates struct {
|
|||||||
Body []api.IssueTemplate `json:"body"`
|
Body []api.IssueTemplate `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IssueBulkResult
|
||||||
|
// swagger:response IssueBulkResult
|
||||||
|
type swaggerResponseIssueBulkResult struct {
|
||||||
|
// in:body
|
||||||
|
Body api.IssueBulkResult `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// StopWatch
|
// StopWatch
|
||||||
// swagger:response StopWatch
|
// swagger:response StopWatch
|
||||||
type swaggerResponseStopWatch struct {
|
type swaggerResponseStopWatch struct {
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ import (
|
|||||||
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
||||||
opts := utils.GetListOptions(ctx)
|
opts := utils.GetListOptions(ctx)
|
||||||
|
|
||||||
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
searchOpts := repo_model.SearchRepoOptions{
|
||||||
Actor: u,
|
Actor: u,
|
||||||
Private: private,
|
Private: private,
|
||||||
ListOptions: opts,
|
ListOptions: opts,
|
||||||
OrderBy: "id ASC",
|
OrderBy: "id ASC",
|
||||||
})
|
}
|
||||||
|
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
|
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
private := ctx.IsSigned
|
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
|
||||||
listUserRepos(ctx, ctx.ContextUser, private)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListMyRepos - list the repositories you own or have access to.
|
// ListMyRepos - list the repositories you own or have access to.
|
||||||
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
|
|||||||
Private: ctx.IsSigned,
|
Private: ctx.IsSigned,
|
||||||
IncludeDescription: true,
|
IncludeDescription: true,
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ import (
|
|||||||
// getStarredRepos returns the repos that the user with the specified userID has
|
// getStarredRepos returns the repos that the user with the specified userID has
|
||||||
// starred
|
// starred
|
||||||
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
|
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
|
||||||
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
|
opts := &repo_model.StarredReposOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
StarrerID: user.ID,
|
StarrerID: user.ID,
|
||||||
IncludePrivate: private,
|
IncludePrivate: private,
|
||||||
})
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
|
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
|
|||||||
maxResults = 1
|
maxResults = 1
|
||||||
users = []*user_model.User{user_model.NewActionsUser()}
|
users = []*user_model.User{user_model.NewActionsUser()}
|
||||||
default:
|
default:
|
||||||
var visible []structs.VisibleType
|
opts := user_model.SearchUserOptions{
|
||||||
if ctx.PublicOnly {
|
|
||||||
visible = []structs.VisibleType{structs.VisibleTypePublic}
|
|
||||||
}
|
|
||||||
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Keyword: ctx.FormTrim("q"),
|
Keyword: ctx.FormTrim("q"),
|
||||||
UID: uid,
|
UID: uid,
|
||||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||||
SearchByEmail: true,
|
SearchByEmail: true,
|
||||||
Visible: visible,
|
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
})
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
users, maxResults, err = user_model.SearchUsers(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
ctx.JSON(http.StatusInternalServerError, map[string]any{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
|||||||
Date: ctx.FormString("date"),
|
Date: ctx.FormString("date"),
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
}
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ import (
|
|||||||
|
|
||||||
// getWatchedRepos returns the repos that the user with the specified userID is watching
|
// getWatchedRepos returns the repos that the user with the specified userID is watching
|
||||||
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
|
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
|
||||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
|
opts := &repo_model.WatchedReposOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
WatcherID: user.ID,
|
WatcherID: user.ID,
|
||||||
IncludePrivate: private,
|
IncludePrivate: private,
|
||||||
})
|
}
|
||||||
|
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||||
|
|
||||||
|
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/updatechecker"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
@@ -135,8 +136,15 @@ func prepareStartupProblemsAlert(ctx *context.Context) {
|
|||||||
func Dashboard(ctx *context.Context) {
|
func Dashboard(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
|
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
|
||||||
ctx.Data["PageIsAdminDashboard"] = true
|
ctx.Data["PageIsAdminDashboard"] = true
|
||||||
// MokoGitea: upstream update checker removed — this is an independent fork
|
|
||||||
ctx.Data["NeedUpdate"] = false
|
// MokoGitea update checker
|
||||||
|
info := updatechecker.GetUpdateInfo()
|
||||||
|
ctx.Data["NeedUpdate"] = info.UpdateAvailable
|
||||||
|
ctx.Data["LatestVersion"] = info.LatestVersion
|
||||||
|
ctx.Data["ReleaseURL"] = info.ReleaseURL
|
||||||
|
ctx.Data["UpdateChannel"] = info.Channel
|
||||||
|
ctx.Data["DockerImage"] = info.DockerImage
|
||||||
|
|
||||||
updateSystemStatus()
|
updateSystemStatus()
|
||||||
ctx.Data["SysStatus"] = sysStatus
|
ctx.Data["SysStatus"] = sysStatus
|
||||||
ctx.Data["SSH"] = setting.SSH
|
ctx.Data["SSH"] = setting.SSH
|
||||||
|
|||||||
@@ -561,6 +561,13 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if grant.ApplicationID != app.ID {
|
||||||
|
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||||
|
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
|
||||||
|
ErrorDescription: "refresh token belongs to a different client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// check if token got already used
|
// check if token got already used
|
||||||
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
|
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
|
||||||
@@ -630,6 +637,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if authorizationCode.RedirectURI != "" && form.RedirectURI != authorizationCode.RedirectURI {
|
||||||
|
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||||
|
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
|
||||||
|
ErrorDescription: "redirect_uri differs from the original authorization request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
// check if granted for this application
|
// check if granted for this application
|
||||||
if authorizationCode.Grant.ApplicationID != app.ID {
|
if authorizationCode.Grant.ApplicationID != app.ID {
|
||||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
@@ -21,6 +22,17 @@ import (
|
|||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) {
|
||||||
|
switch unitType {
|
||||||
|
case unit.TypeIssues, unit.TypePullRequests:
|
||||||
|
return auth_model.AccessTokenScopeReadIssue, true
|
||||||
|
case unit.TypeReleases:
|
||||||
|
return auth_model.AccessTokenScopeReadRepository, true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UploadIssueAttachment response for Issue/PR attachments
|
// UploadIssueAttachment response for Issue/PR attachments
|
||||||
func UploadIssueAttachment(ctx *context.Context) {
|
func UploadIssueAttachment(ctx *context.Context) {
|
||||||
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
|
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
|
||||||
@@ -150,9 +162,12 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else { // If we have the linked type, we need to check access
|
} else { // If we have the linked type, we need to check access
|
||||||
var perm access_model.Permission
|
var (
|
||||||
if ctx.Repo.Repository == nil {
|
perm access_model.Permission
|
||||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
repo = ctx.Repo.Repository
|
||||||
|
)
|
||||||
|
if repo == nil {
|
||||||
|
repo, err = repo_model.GetRepositoryByID(ctx, repoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetRepositoryByID", err)
|
ctx.ServerError("GetRepositoryByID", err)
|
||||||
return
|
return
|
||||||
@@ -170,6 +185,13 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
|||||||
ctx.HTTPError(http.StatusNotFound)
|
ctx.HTTPError(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||||
|
context.CheckTokenScopes(ctx, repo, requiredScope)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := attach.IncreaseDownloadCount(ctx); err != nil {
|
if err := attach.IncreaseDownloadCount(ctx); err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
@@ -18,6 +19,11 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func checkDownloadTokenScope(ctx *context.Context) bool {
|
||||||
|
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||||
|
return !ctx.Written()
|
||||||
|
}
|
||||||
|
|
||||||
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
|
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
|
||||||
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error {
|
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error {
|
||||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||||
@@ -88,6 +94,10 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
|
|||||||
|
|
||||||
// SingleDownload download a file by repos path
|
// SingleDownload download a file by repos path
|
||||||
func SingleDownload(ctx *context.Context) {
|
func SingleDownload(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
blob, lastModified := getBlobForEntry(ctx)
|
blob, lastModified := getBlobForEntry(ctx)
|
||||||
if blob == nil {
|
if blob == nil {
|
||||||
return
|
return
|
||||||
@@ -100,6 +110,10 @@ func SingleDownload(ctx *context.Context) {
|
|||||||
|
|
||||||
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
|
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
|
||||||
func SingleDownloadOrLFS(ctx *context.Context) {
|
func SingleDownloadOrLFS(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
blob, lastModified := getBlobForEntry(ctx)
|
blob, lastModified := getBlobForEntry(ctx)
|
||||||
if blob == nil {
|
if blob == nil {
|
||||||
return
|
return
|
||||||
@@ -112,6 +126,10 @@ func SingleDownloadOrLFS(ctx *context.Context) {
|
|||||||
|
|
||||||
// DownloadByID download a file by sha1 ID
|
// DownloadByID download a file by sha1 ID
|
||||||
func DownloadByID(ctx *context.Context) {
|
func DownloadByID(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
@@ -128,6 +146,10 @@ func DownloadByID(ctx *context.Context) {
|
|||||||
|
|
||||||
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
|
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
|
||||||
func DownloadByIDOrLFS(ctx *context.Context) {
|
func DownloadByIDOrLFS(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if repoExist {
|
if repoExist {
|
||||||
// Because of special ref "refs/for" (agit) , need delay write permission check
|
// Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks.
|
||||||
if git.DefaultFeatures().SupportProcReceive {
|
if git.DefaultFeatures().SupportProcReceive && !isWiki {
|
||||||
accessMode = perm.AccessModeRead
|
accessMode = perm.AccessModeRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ var IssueTemplateCandidates = []string{
|
|||||||
"issue_template.md",
|
"issue_template.md",
|
||||||
"issue_template.yaml",
|
"issue_template.yaml",
|
||||||
"issue_template.yml",
|
"issue_template.yml",
|
||||||
|
".mokogitea/ISSUE_TEMPLATE.md",
|
||||||
|
".mokogitea/ISSUE_TEMPLATE.yaml",
|
||||||
|
".mokogitea/ISSUE_TEMPLATE.yml",
|
||||||
|
".mokogitea/issue_template.md",
|
||||||
|
".mokogitea/issue_template.yaml",
|
||||||
|
".mokogitea/issue_template.yml",
|
||||||
".gitea/ISSUE_TEMPLATE.md",
|
".gitea/ISSUE_TEMPLATE.md",
|
||||||
".gitea/ISSUE_TEMPLATE.yaml",
|
".gitea/ISSUE_TEMPLATE.yaml",
|
||||||
".gitea/ISSUE_TEMPLATE.yml",
|
".gitea/ISSUE_TEMPLATE.yml",
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ var pullRequestTemplateCandidates = []string{
|
|||||||
"pull_request_template.md",
|
"pull_request_template.md",
|
||||||
"pull_request_template.yaml",
|
"pull_request_template.yaml",
|
||||||
"pull_request_template.yml",
|
"pull_request_template.yml",
|
||||||
|
".mokogitea/PULL_REQUEST_TEMPLATE.md",
|
||||||
|
".mokogitea/PULL_REQUEST_TEMPLATE.yaml",
|
||||||
|
".mokogitea/PULL_REQUEST_TEMPLATE.yml",
|
||||||
|
".mokogitea/pull_request_template.md",
|
||||||
|
".mokogitea/pull_request_template.yaml",
|
||||||
|
".mokogitea/pull_request_template.yml",
|
||||||
".gitea/PULL_REQUEST_TEMPLATE.md",
|
".gitea/PULL_REQUEST_TEMPLATE.md",
|
||||||
".gitea/PULL_REQUEST_TEMPLATE.yaml",
|
".gitea/PULL_REQUEST_TEMPLATE.yaml",
|
||||||
".gitea/PULL_REQUEST_TEMPLATE.yml",
|
".gitea/PULL_REQUEST_TEMPLATE.yml",
|
||||||
|
|||||||
@@ -364,6 +364,10 @@ func RedirectDownload(ctx *context.Context) {
|
|||||||
|
|
||||||
// Download an archive of a repository
|
// Download an archive of a repository
|
||||||
func Download(ctx *context.Context) {
|
func Download(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path"))
|
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, util.ErrInvalidArgument) {
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
@@ -389,6 +393,10 @@ func Download(ctx *context.Context) {
|
|||||||
// a request that's already in-progress, but the archiver service will just
|
// a request that's already in-progress, but the archiver service will just
|
||||||
// kind of drop it on the floor if this is the case.
|
// kind of drop it on the floor if this is the case.
|
||||||
func InitiateDownload(ctx *context.Context) {
|
func InitiateDownload(ctx *context.Context) {
|
||||||
|
if !checkDownloadTokenScope(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
paths := ctx.FormStrings("path")
|
paths := ctx.FormStrings("path")
|
||||||
if setting.Repository.StreamArchives || len(paths) > 0 {
|
if setting.Repository.StreamArchives || len(paths) > 0 {
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
ctx.JSON(http.StatusOK, map[string]any{
|
||||||
|
|||||||
@@ -35,11 +35,16 @@ func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.Act
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
concurrencyGroup, concurrencyCancel, err := jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs)
|
||||||
attempt.ConcurrencyGroup, attempt.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("evaluate concurrency: %w", err)
|
return fmt.Errorf("evaluate concurrency: %w", err)
|
||||||
}
|
}
|
||||||
|
run.ConcurrencyGroup = concurrencyGroup
|
||||||
|
run.ConcurrencyCancel = concurrencyCancel
|
||||||
|
if attempt != nil {
|
||||||
|
attempt.ConcurrencyGroup = concurrencyGroup
|
||||||
|
attempt.ConcurrencyCancel = concurrencyCancel
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,15 @@ func TestEvaluateRunConcurrency_RunIDFallback(t *testing.T) {
|
|||||||
CancelInProgress: "true",
|
CancelInProgress: "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runA, expr, nil, nil))
|
attemptA := &actions_model.ActionRunAttempt{RunID: runA.ID, RepoID: runA.RepoID}
|
||||||
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runB, expr, nil, nil))
|
attemptB := &actions_model.ActionRunAttempt{RunID: runB.ID, RepoID: runB.RepoID}
|
||||||
|
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runA, attemptA, expr, nil, nil))
|
||||||
|
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runB, attemptB, expr, nil, nil))
|
||||||
|
|
||||||
assert.Contains(t, runA.ConcurrencyGroup, "791")
|
assert.Contains(t, runA.ConcurrencyGroup, "791")
|
||||||
assert.Contains(t, runB.ConcurrencyGroup, "792")
|
assert.Contains(t, runB.ConcurrencyGroup, "792")
|
||||||
|
assert.Equal(t, runA.ConcurrencyGroup, attemptA.ConcurrencyGroup)
|
||||||
|
assert.Equal(t, runB.ConcurrencyGroup, attemptB.ConcurrencyGroup)
|
||||||
assert.NotEqual(t, runA.ConcurrencyGroup, runB.ConcurrencyGroup)
|
assert.NotEqual(t, runA.ConcurrencyGroup, runB.ConcurrencyGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if wfRawConcurrency != nil {
|
if wfRawConcurrency != nil {
|
||||||
if err := EvaluateRunConcurrencyFillModel(ctx, run, nil, wfRawConcurrency, vars, inputs); err != nil {
|
attempt := &actions_model.ActionRunAttempt{RunID: run.ID, RepoID: run.RepoID}
|
||||||
|
if err := EvaluateRunConcurrencyFillModel(ctx, run, attempt, wfRawConcurrency, vars, inputs); err != nil {
|
||||||
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
||||||
}
|
}
|
||||||
run.Status, _, err = PrepareToStartRunWithConcurrency(ctx, &actions_model.ActionRunAttempt{RunID: run.ID})
|
run.Status, _, err = PrepareToStartRunWithConcurrency(ctx, attempt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,14 +132,22 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS
|
|||||||
}
|
}
|
||||||
|
|
||||||
func withScheduleInEventPayload(eventPayload, schedule string) string {
|
func withScheduleInEventPayload(eventPayload, schedule string) string {
|
||||||
if schedule == "" || eventPayload == "" {
|
if schedule == "" {
|
||||||
return eventPayload
|
return eventPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
event := map[string]any{}
|
// eventPayload originates from json.Marshal(input.Payload) in handleSchedules,
|
||||||
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
|
// so a nil payload is stored as the literal "null" and pre-existing rows may be
|
||||||
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
|
// empty. Both cases start from a fresh map so the schedule field can still be set.
|
||||||
return eventPayload
|
var event map[string]any
|
||||||
|
if eventPayload != "" {
|
||||||
|
if err := json.Unmarshal([]byte(eventPayload), &event); err != nil {
|
||||||
|
log.Error("withScheduleInEventPayload: unmarshal: %v", err)
|
||||||
|
return eventPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event == nil {
|
||||||
|
event = map[string]any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
event["schedule"] = schedule
|
event["schedule"] = schedule
|
||||||
|
|||||||
@@ -22,9 +22,20 @@ func TestWithScheduleInEventPayload(t *testing.T) {
|
|||||||
assert.Equal(t, "refs/heads/main", event["ref"])
|
assert.Equal(t, "refs/heads/main", event["ref"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("keeps empty payload", func(t *testing.T) {
|
t.Run("adds schedule to null payload", func(t *testing.T) {
|
||||||
|
updated := withScheduleInEventPayload("null", "37 12 5 1 2")
|
||||||
|
|
||||||
|
event := map[string]any{}
|
||||||
|
assert.NoError(t, json.Unmarshal([]byte(updated), &event))
|
||||||
|
assert.Equal(t, "37 12 5 1 2", event["schedule"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adds schedule to empty payload", func(t *testing.T) {
|
||||||
updated := withScheduleInEventPayload("", "37 12 5 1 2")
|
updated := withScheduleInEventPayload("", "37 12 5 1 2")
|
||||||
assert.Empty(t, updated)
|
|
||||||
|
event := map[string]any{}
|
||||||
|
assert.NoError(t, json.Unmarshal([]byte(updated), &event))
|
||||||
|
assert.Equal(t, "37 12 5 1 2", event["schedule"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("keeps payload when schedule empty", func(t *testing.T) {
|
t.Run("keeps payload when schedule empty", func(t *testing.T) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
@@ -47,6 +48,12 @@ type APIContext struct {
|
|||||||
PublicOnly bool // Whether the request is for a public endpoint
|
PublicOnly bool // Whether the request is for a public endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
|
||||||
|
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
|
||||||
|
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
|
||||||
|
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
|
||||||
return req.Context().Value(apiContextKey).(*APIContext)
|
return req.Context().Value(apiContextKey).(*APIContext)
|
||||||
|
|||||||
@@ -12,6 +12,39 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
|
||||||
|
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
|
||||||
|
if ctx.Data["IsApiToken"] != true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publicOnly, err := scope.PublicOnly()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("PublicOnly", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicOnly && repo != nil && repo.IsPrivate {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeMatched, err := scope.HasAnyScope(scopes...)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("HasAnyScope", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scopeMatched {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RequireRepoAdmin returns a middleware for requiring repository admin permission
|
// RequireRepoAdmin returns a middleware for requiring repository admin permission
|
||||||
func RequireRepoAdmin() func(ctx *Context) {
|
func RequireRepoAdmin() func(ctx *Context) {
|
||||||
return func(ctx *Context) {
|
return func(ctx *Context) {
|
||||||
@@ -57,39 +90,7 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRepoScopedToken check whether personal access token has repo scope
|
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
|
||||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
|
||||||
if ok { // it's a personal access token but not oauth2 token
|
|
||||||
var scopeMatched bool
|
|
||||||
|
|
||||||
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
|
|
||||||
|
|
||||||
// check if scope only applies to public resources
|
|
||||||
publicOnly, err := scope.PublicOnly()
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("HasScope", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if publicOnly && repo.IsPrivate {
|
|
||||||
ctx.HTTPError(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeMatched, err = scope.HasScope(requiredScopes...)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("HasScope", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !scopeMatched {
|
|
||||||
ctx.HTTPError(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/webhook"
|
"code.gitea.io/gitea/models/webhook"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/updatechecker"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
"code.gitea.io/gitea/services/migrations"
|
"code.gitea.io/gitea/services/migrations"
|
||||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||||
@@ -183,4 +184,17 @@ func initBasicTasks() {
|
|||||||
registerCleanupPackages()
|
registerCleanupPackages()
|
||||||
}
|
}
|
||||||
registerSyncRepoLicenses()
|
registerSyncRepoLicenses()
|
||||||
|
if setting.UpdateChecker.Enabled {
|
||||||
|
registerUpdateChecker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerUpdateChecker() {
|
||||||
|
RegisterTaskFatal("update_checker", &BaseConfig{
|
||||||
|
Enabled: true,
|
||||||
|
RunAtStart: true,
|
||||||
|
Schedule: "@every 24h",
|
||||||
|
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
|
||||||
|
return updatechecker.CheckForUpdate()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import (
|
|||||||
var templateDirCandidates = []string{
|
var templateDirCandidates = []string{
|
||||||
"ISSUE_TEMPLATE",
|
"ISSUE_TEMPLATE",
|
||||||
"issue_template",
|
"issue_template",
|
||||||
|
".mokogitea/ISSUE_TEMPLATE",
|
||||||
|
".mokogitea/issue_template",
|
||||||
".gitea/ISSUE_TEMPLATE",
|
".gitea/ISSUE_TEMPLATE",
|
||||||
".gitea/issue_template",
|
".gitea/issue_template",
|
||||||
".github/ISSUE_TEMPLATE",
|
".github/ISSUE_TEMPLATE",
|
||||||
@@ -32,6 +34,8 @@ var templateDirCandidates = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var templateConfigCandidates = []string{
|
var templateConfigCandidates = []string{
|
||||||
|
".mokogitea/ISSUE_TEMPLATE/config",
|
||||||
|
".mokogitea/issue_template/config",
|
||||||
".gitea/ISSUE_TEMPLATE/config",
|
".gitea/ISSUE_TEMPLATE/config",
|
||||||
".gitea/issue_template/config",
|
".gitea/issue_template/config",
|
||||||
".github/ISSUE_TEMPLATE/config",
|
".github/ISSUE_TEMPLATE/config",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -605,6 +606,18 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
|||||||
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
|
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !u.IsActive || u.ProhibitLogin {
|
||||||
|
return nil, util.NewPermissionDeniedErrorf("not allowed to access any repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
perm, err := access_model.GetDoerRepoPermission(ctx, target, u)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to GetDoerRepoPermission for user[%d] repo[%d]: %v", claims.UserID, target.ID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !perm.CanAccess(mode, unit.TypeCode) {
|
||||||
|
return nil, util.NewPermissionDeniedErrorf("no permission to access the repository")
|
||||||
|
}
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
perm_model "code.gitea.io/gitea/models/perm"
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -22,11 +24,15 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
func TestAuthenticate(t *testing.T) {
|
func TestAuthenticate(t *testing.T) {
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
ctx, _ := contexttest.MockContext(t, "/")
|
||||||
|
|
||||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1})
|
getUserToken := func(op string, userID int64, repo *repo_model.Repository) string {
|
||||||
_, token2, _ = strings.Cut(token2, " ")
|
s, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: op, UserID: userID, RepoID: repo.ID})
|
||||||
ctx, _ := contexttest.MockContext(t, "/")
|
_, token, _ := strings.Cut(s, " ")
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("handleLFSToken", func(t *testing.T) {
|
t.Run("handleLFSToken", func(t *testing.T) {
|
||||||
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
|
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
|
||||||
@@ -37,15 +43,62 @@ func TestAuthenticate(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, u)
|
assert.Nil(t, u)
|
||||||
|
|
||||||
u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead)
|
u, err = handleLFSToken(ctx, getUserToken("download", 2, repo1), repo1, perm_model.AccessModeRead)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.EqualValues(t, 2, u.ID)
|
assert.EqualValues(t, 2, u.ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("authenticate", func(t *testing.T) {
|
t.Run("authenticate", func(t *testing.T) {
|
||||||
const prefixBearer = "Bearer "
|
const prefixBearer = "Bearer "
|
||||||
|
token := getUserToken("download", 2, repo1)
|
||||||
assert.False(t, authenticate(ctx, repo1, "", true, false))
|
assert.False(t, authenticate(ctx, repo1, "", true, false))
|
||||||
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
|
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
|
||||||
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
|
assert.True(t, authenticate(ctx, repo1, prefixBearer+token, true, false))
|
||||||
|
})
|
||||||
|
|
||||||
|
handleLFSTokenTestPerm := func(op string, userID int64, repo *repo_model.Repository, accessMode perm_model.AccessMode) error {
|
||||||
|
token := getUserToken(op, userID, repo)
|
||||||
|
u, err := handleLFSToken(ctx, token, repo, accessMode)
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, userID, u.ID)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("handleLFSToken blocks prohibited users", func(t *testing.T) {
|
||||||
|
user37 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 37})
|
||||||
|
|
||||||
|
// prohibited user
|
||||||
|
assert.True(t, user37.ProhibitLogin)
|
||||||
|
err := handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
|
||||||
|
assert.ErrorContains(t, err, "not allowed to access any repository")
|
||||||
|
|
||||||
|
// normal user
|
||||||
|
_, _ = db.GetEngine(t.Context()).ID(37).Cols("prohibit_login").Update(&user_model.User{ProhibitLogin: false})
|
||||||
|
err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// inactive user
|
||||||
|
_, _ = db.GetEngine(t.Context()).ID(37).Cols("is_active").Update(&user_model.User{IsActive: false})
|
||||||
|
err = handleLFSTokenTestPerm("download", 37, repo1, perm_model.AccessModeRead)
|
||||||
|
assert.ErrorContains(t, err, "not allowed to access any repository")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handleLFSToken blocks users without repo access", func(t *testing.T) {
|
||||||
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
err := handleLFSTokenTestPerm("download", 10, repo2, perm_model.AccessModeRead)
|
||||||
|
assert.ErrorContains(t, err, "no permission to access the repository")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handleLFSToken requires write access for uploads", func(t *testing.T) {
|
||||||
|
err := handleLFSTokenTestPerm("download", 10, repo1, perm_model.AccessModeRead)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = handleLFSTokenTestPerm("upload", 10, repo1, perm_model.AccessModeWrite)
|
||||||
|
assert.ErrorContains(t, err, "no permission to access the repository")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handleLFSToken allows writes for authorized users", func(t *testing.T) {
|
||||||
|
err := handleLFSTokenTestPerm("upload", 2, repo1, perm_model.AccessModeWrite)
|
||||||
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
net_mail "net/mail"
|
net_mail "net/mail"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,31 +23,10 @@ import (
|
|||||||
"github.com/jhillyerd/enmime/v2"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
addressTokenRegex *regexp.Regexp
|
|
||||||
referenceTokenRegex *regexp.Regexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
if !setting.IncomingEmail.Enabled {
|
if !setting.IncomingEmail.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
addressTokenRegex, err = regexp.Compile(
|
|
||||||
fmt.Sprintf(
|
|
||||||
`\A%s\z`,
|
|
||||||
strings.Replace(regexp.QuoteMeta(setting.IncomingEmail.ReplyToAddress), regexp.QuoteMeta(setting.IncomingEmail.TokenPlaceholder), "(.+)", 1),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
referenceTokenRegex, err = regexp.Compile(fmt.Sprintf(`\Areply-(.+)@%s\z`, regexp.QuoteMeta(setting.Domain)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
|
ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
|
||||||
defer finished()
|
defer finished()
|
||||||
@@ -241,7 +219,7 @@ loop:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerType, user, payload, err := token.ExtractToken(ctx, t)
|
handlerType, user, payload, err := token.DecodeToken(ctx, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*token.ErrToken); ok {
|
if _, ok := err.(*token.ErrToken); ok {
|
||||||
log.Info("Invalid incoming email token: %v", err)
|
log.Info("Invalid incoming email token: %v", err)
|
||||||
@@ -292,22 +270,31 @@ func isAutomaticReply(env *enmime.Envelope) bool {
|
|||||||
return autoRespond != ""
|
return autoRespond != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractToken(s, tokenPrefix, tokenSuffix string) string {
|
||||||
|
if len(s) <= len(tokenPrefix)+len(tokenSuffix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
prefix, suffix := s[0:len(tokenPrefix)], s[len(s)-len(tokenSuffix):]
|
||||||
|
if strings.EqualFold(prefix, tokenPrefix) && strings.EqualFold(suffix, tokenSuffix) {
|
||||||
|
return s[len(tokenPrefix) : len(s)-len(tokenSuffix)]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// searchTokenInHeaders looks for the token in To, Delivered-To and References
|
// searchTokenInHeaders looks for the token in To, Delivered-To and References
|
||||||
func searchTokenInHeaders(env *enmime.Envelope) string {
|
func searchTokenInHeaders(env *enmime.Envelope) string {
|
||||||
if addressTokenRegex != nil {
|
to, _ := env.AddressList("To")
|
||||||
to, _ := env.AddressList("To")
|
|
||||||
|
|
||||||
token := searchTokenInAddresses(to)
|
token := searchTokenInAddresses(to)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
deliveredTo, _ := env.AddressList("Delivered-To")
|
deliveredTo, _ := env.AddressList("Delivered-To")
|
||||||
|
|
||||||
token = searchTokenInAddresses(deliveredTo)
|
token = searchTokenInAddresses(deliveredTo)
|
||||||
if token != "" {
|
if token != "" {
|
||||||
return token
|
return token
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
references := env.GetHeader("References")
|
references := env.GetHeader("References")
|
||||||
@@ -322,10 +309,9 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
|||||||
if end == -1 || begin > end {
|
if end == -1 || begin > end {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
t := extractToken(references[begin:end], "reply-", "@"+setting.Domain)
|
||||||
match := referenceTokenRegex.FindStringSubmatch(references[begin:end])
|
if t != "" {
|
||||||
if len(match) == 2 {
|
return t
|
||||||
return match[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
references = references[end+1:]
|
references = references[end+1:]
|
||||||
@@ -336,15 +322,15 @@ func searchTokenInHeaders(env *enmime.Envelope) string {
|
|||||||
|
|
||||||
// searchTokenInAddresses looks for the token in an address
|
// searchTokenInAddresses looks for the token in an address
|
||||||
func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
func searchTokenInAddresses(addresses []*net_mail.Address) string {
|
||||||
for _, address := range addresses {
|
tokenPrefix, tokenSuffix, _ := strings.Cut(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder)
|
||||||
match := addressTokenRegex.FindStringSubmatch(address.Address)
|
if tokenSuffix == "" {
|
||||||
if len(match) != 2 {
|
return ""
|
||||||
continue
|
}
|
||||||
}
|
for _, address := range addresses {
|
||||||
|
if t := extractToken(address.Address, tokenPrefix, tokenSuffix); t != "" {
|
||||||
return match[1]
|
return t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/jhillyerd/enmime/v2"
|
"github.com/jhillyerd/enmime/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -68,6 +70,18 @@ func TestIsAutomaticReply(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchTokenInHeadersCaseInsensitive(t *testing.T) {
|
||||||
|
setting.IncomingEmail.ReplyToAddress = "InComing+%{token}@ExAmPle.com"
|
||||||
|
setting.Domain = "DoMain.com"
|
||||||
|
mkEnv := func(s string) *enmime.Envelope {
|
||||||
|
env, _ := enmime.ReadEnvelope(strings.NewReader(s + "\r\n\r\n"))
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("To: incoming+abc@EXAMPLE.COM")))
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("Delivered-To: INCOMING+abc@example.com")))
|
||||||
|
assert.Equal(t, "abc", searchTokenInHeaders(mkEnv("References: <ReplY-abc@DomaiN.COM>")))
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetContentFromMailReader(t *testing.T) {
|
func TestGetContentFromMailReader(t *testing.T) {
|
||||||
mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||||
msg.ReplyTo = replyAddress
|
msg.ReplyTo = replyAddress
|
||||||
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
|
msg.SetHeader("List-Post", fmt.Sprintf("<mailto:%s>", replyAddress))
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("CreateToken failed: %v", err)
|
log.Error("CreateToken failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
|
unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1)
|
||||||
listUnsubscribe = append(listUnsubscribe, "<mailto:"+unsubAddress+">")
|
listUnsubscribe = append(listUnsubscribe, "<mailto:"+unsubAddress+">")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@@ -73,9 +74,11 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
|
|||||||
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractToken extracts the action/user tuple from the token and verifies the content
|
// DecodeToken decodes the handler, user and payload from the token and verifies the content
|
||||||
func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
func DecodeToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
|
||||||
data, err := encodingWithoutPadding.DecodeString(token)
|
// MTAs are permitted to alter the case of the local-part (RFC 5321 §2.4), so normalize
|
||||||
|
// to the base32 alphabet before decoding to survive a lowercased reply-to address.
|
||||||
|
data, err := encodingWithoutPadding.DecodeString(strings.ToUpper(token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UnknownHandlerType, nil, nil, err
|
return UnknownHandlerType, nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -118,11 +121,11 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
|||||||
return handlerType, user, innerPayload, nil
|
return handlerType, user, innerPayload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateHmac creates a trunkated HMAC for the given payload
|
// generateHmac creates a truncated HMAC for the given payload
|
||||||
func generateHmac(secret, payload []byte) []byte {
|
func generateHmac(secret, payload []byte) []byte {
|
||||||
mac := crypto_hmac.New(sha256.New, secret)
|
mac := crypto_hmac.New(sha256.New, secret)
|
||||||
mac.Write(payload)
|
mac.Write(payload)
|
||||||
hmac := mac.Sum(nil)
|
hmac := mac.Sum(nil)
|
||||||
|
|
||||||
return hmac[:10] // RFC2104 recommends not using less then 80 bits
|
return hmac[:10] // RFC2104 recommends not using less than 80 bits
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package ntfy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if setting.Ntfy.Enabled {
|
||||||
|
notify_service.RegisterNotifier(NewNotifier())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ntfyNotifier struct {
|
||||||
|
notify_service.NullNotifier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotifier creates a new ntfy notifier.
|
||||||
|
func NewNotifier() notify_service.Notifier {
|
||||||
|
return &ntfyNotifier{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) Run() {}
|
||||||
|
|
||||||
|
func repoTopic(repo *repo_model.Repository) string {
|
||||||
|
if repo == nil {
|
||||||
|
return setting.Ntfy.DefaultTopic
|
||||||
|
}
|
||||||
|
return setting.Ntfy.DefaultTopic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) NewIssue(_ context.Context, issue *issues_model.Issue, _ []*user_model.User) {
|
||||||
|
_ = issue.LoadRepo(context.Background())
|
||||||
|
SendAsync(repoTopic(issue.Repo),
|
||||||
|
fmt.Sprintf("New Issue: %s", issue.Title),
|
||||||
|
fmt.Sprintf("#%d in %s\n%s", issue.Index, issue.Repo.FullName(), issue.Content),
|
||||||
|
"default",
|
||||||
|
"issue,new")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) IssueChangeStatus(_ context.Context, doer *user_model.User, _ string, issue *issues_model.Issue, _ *issues_model.Comment, closeOrReopen bool) {
|
||||||
|
_ = issue.LoadRepo(context.Background())
|
||||||
|
action := "reopened"
|
||||||
|
if !closeOrReopen {
|
||||||
|
action = "closed"
|
||||||
|
}
|
||||||
|
SendAsync(repoTopic(issue.Repo),
|
||||||
|
fmt.Sprintf("Issue %s: %s", action, issue.Title),
|
||||||
|
fmt.Sprintf("#%d %s by %s", issue.Index, action, doer.Name),
|
||||||
|
"low",
|
||||||
|
"issue,"+action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) NewPullRequest(_ context.Context, pr *issues_model.PullRequest, _ []*user_model.User) {
|
||||||
|
_ = pr.LoadIssue(context.Background())
|
||||||
|
_ = pr.Issue.LoadRepo(context.Background())
|
||||||
|
SendAsync(repoTopic(pr.Issue.Repo),
|
||||||
|
fmt.Sprintf("New PR: %s", pr.Issue.Title),
|
||||||
|
fmt.Sprintf("#%d in %s\n%s → %s", pr.Issue.Index, pr.Issue.Repo.FullName(), pr.HeadBranch, pr.BaseBranch),
|
||||||
|
"default",
|
||||||
|
"git-pull-request,new")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) MergePullRequest(_ context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||||
|
_ = pr.LoadIssue(context.Background())
|
||||||
|
_ = pr.Issue.LoadRepo(context.Background())
|
||||||
|
SendAsync(repoTopic(pr.Issue.Repo),
|
||||||
|
fmt.Sprintf("PR Merged: %s", pr.Issue.Title),
|
||||||
|
fmt.Sprintf("#%d merged by %s", pr.Issue.Index, doer.Name),
|
||||||
|
"default",
|
||||||
|
"git-merge,merged")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) NewRelease(_ context.Context, rel *repo_model.Release) {
|
||||||
|
SendAsync(repoTopic(rel.Repo),
|
||||||
|
fmt.Sprintf("New Release: %s", rel.TagName),
|
||||||
|
fmt.Sprintf("%s in %s\n%s", rel.TagName, rel.Repo.FullName(), rel.Note),
|
||||||
|
"high",
|
||||||
|
"rocket,release")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ntfyNotifier) WorkflowRunStatusUpdate(_ context.Context, repo *repo_model.Repository, _ *user_model.User, run *actions_model.ActionRun) {
|
||||||
|
if run.Status.String() != "success" && run.Status.String() != "failure" {
|
||||||
|
return // only notify on completion
|
||||||
|
}
|
||||||
|
priority := "default"
|
||||||
|
tags := "white_check_mark,ci"
|
||||||
|
if run.Status.String() == "failure" {
|
||||||
|
priority = "high"
|
||||||
|
tags = "x,ci-fail"
|
||||||
|
}
|
||||||
|
SendAsync(repoTopic(repo),
|
||||||
|
fmt.Sprintf("CI %s: %s", run.Status.String(), run.Title),
|
||||||
|
fmt.Sprintf("Workflow in %s", repo.FullName()),
|
||||||
|
priority,
|
||||||
|
tags)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package ntfy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send publishes a notification to the ntfy server.
|
||||||
|
func Send(topic, title, message, priority, tags string) error {
|
||||||
|
if !setting.Ntfy.Enabled || setting.Ntfy.ServerURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if topic == "" {
|
||||||
|
topic = setting.Ntfy.DefaultTopic
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s", strings.TrimRight(setting.Ntfy.ServerURL, "/"), topic)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, strings.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntfy request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Title", title)
|
||||||
|
if priority != "" {
|
||||||
|
req.Header.Set("Priority", priority)
|
||||||
|
}
|
||||||
|
if tags != "" {
|
||||||
|
req.Header.Set("Tags", tags)
|
||||||
|
}
|
||||||
|
if setting.Ntfy.Token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setting.Ntfy.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntfy send: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("ntfy returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("ntfy notification sent: %s — %s", topic, title)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendAsync sends a notification in a goroutine (non-blocking).
|
||||||
|
func SendAsync(topic, title, message, priority, tags string) {
|
||||||
|
go func() {
|
||||||
|
if err := Send(topic, title, message, priority, tags); err != nil {
|
||||||
|
log.Error("ntfy async send failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -85,6 +85,11 @@ func CreateMigrateTask(ctx context.Context, doer, u *user_model.User, opts base.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
opts.AuthToken = ""
|
opts.AuthToken = ""
|
||||||
|
opts.AWSSecretAccessKeyEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AWSSecretAccessKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.AWSSecretAccessKey = ""
|
||||||
bs, err := json.Marshal(&opts)
|
bs, err := json.Marshal(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
{{template "admin/layout_head" (dict "pageClass" "admin dashboard")}}
|
{{template "admin/layout_head" (dict "pageClass" "admin dashboard")}}
|
||||||
<div class="admin-setting-content">
|
<div class="admin-setting-content">
|
||||||
|
{{if .NeedUpdate}}
|
||||||
|
<div class="ui positive message">
|
||||||
|
<div class="header">{{svg "octicon-info"}} MokoGitea Update Available</div>
|
||||||
|
<p>A new version <strong>{{.LatestVersion}}</strong> is available{{if .UpdateChannel}} ({{.UpdateChannel}} channel){{end}}.
|
||||||
|
{{if .ReleaseURL}}<a href="{{.ReleaseURL}}" target="_blank" rel="noopener noreferrer">View release notes</a>{{end}}</p>
|
||||||
|
{{if .DockerImage}}<p><code>docker pull {{.DockerImage}}</code></p>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<h4 class="ui top attached header">
|
<h4 class="ui top attached header">
|
||||||
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
|
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="navbar-left">
|
<div class="navbar-left">
|
||||||
<!-- the logo -->
|
<!-- the logo -->
|
||||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<meta property="og:title" content="{{AppName}}">
|
<meta property="og:title" content="{{AppName}}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="{{AssetUrlPrefix}}/img/logo.png">
|
<meta property="og:image" content="https://mokoconsulting.tech/images/branding/logo.png">
|
||||||
<meta property="og:url" content="{{ctx.AppFullLink}}">
|
<meta property="og:url" content="{{ctx.AppFullLink}}">
|
||||||
<meta property="og:description" content="{{MetaDescription}}">
|
<meta property="og:description" content="{{MetaDescription}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
|
{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="large tw-text-balance">
|
<p class="large tw-text-balance">
|
||||||
{{ctx.Locale.Tr "startpage.install_desc" "{{HelpURL}}/installation/install-from-binary" "https://github.com/go-gitea/gitea/tree/master/docker" "{{HelpURL}}/installation/install-from-package"}}
|
{{ctx.Locale.Tr "startpage.install_desc" "{{HelpURL}}/installation/install-from-binary" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea/src/branch/main/docker" "{{HelpURL}}/installation/install-from-package"}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="eight wide center column">
|
<div class="eight wide center column">
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
|
{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="large tw-text-balance">
|
<p class="large tw-text-balance">
|
||||||
{{ctx.Locale.Tr "startpage.license_desc" "https://code.gitea.io/gitea" "code.gitea.io/gitea" "https://github.com/go-gitea/gitea"}}
|
{{ctx.Locale.Tr "startpage.license_desc" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea" "MokoConsulting/MokoGitea" "https://git.mokoconsulting.tech/MokoConsulting/MokoGitea"}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,12 +10,28 @@
|
|||||||
{{template "user/overview/header" .}}
|
{{template "user/overview/header" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p>
|
<p>
|
||||||
|
<a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a>
|
||||||
|
<span class="label-list">
|
||||||
|
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
|
||||||
|
</span>
|
||||||
|
/ <strong>{{ctx.Locale.Tr "repo.settings"}}</strong>
|
||||||
|
</p>
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "packages.settings.visibility"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<p>{{ctx.Locale.Tr "packages.settings.visibility.inherit"}}</p>
|
||||||
|
<a class="ui basic button" href="{{.ContextUser.SettingsLink}}">{{ctx.Locale.Tr "packages.settings.visibility.button"}}</a>
|
||||||
|
</div>
|
||||||
<h4 class="ui top attached header">
|
<h4 class="ui top attached header">
|
||||||
{{ctx.Locale.Tr "packages.settings.link"}}
|
{{ctx.Locale.Tr "packages.settings.link"}}
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<p>{{ctx.Locale.Tr "packages.settings.link.description"}}</p>
|
<p>{{ctx.Locale.Tr "packages.settings.link.description"}}</p>
|
||||||
|
<p>- {{ctx.Locale.Tr "packages.settings.link.notice1"}}</p>
|
||||||
|
<p>- {{ctx.Locale.Tr "packages.settings.link.notice2"}}</p>
|
||||||
|
<p>- {{ctx.Locale.Tr "packages.settings.link.notice3"}}</p>
|
||||||
<form class="ui form form-fetch-action ignore-dirty flex-text-block" action="{{.Link}}" method="post">
|
<form class="ui form form-fetch-action ignore-dirty flex-text-block" action="{{.Link}}" method="post">
|
||||||
<input type="hidden" name="action" value="link">
|
<input type="hidden" name="action" value="link">
|
||||||
<div data-global-init="initSearchRepoBox" class="ui search" data-uid="{{.PackageDescriptor.Owner.ID}}">
|
<div data-global-init="initSearchRepoBox" class="ui search" data-uid="{{.PackageDescriptor.Owner.ID}}">
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
{{range .VersionsToRemove}}
|
{{range .VersionsToRemove}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.Package.Type.Name}}</td>
|
<td>{{.Package.Type.Name}}</td>
|
||||||
<td>{{.Package.Name}}</td>
|
<td>
|
||||||
|
{{.Package.Name}}
|
||||||
|
<span class="label-list">
|
||||||
|
{{template "package/shared/visibility_badge" dict "Package" .Package "Owner" .Owner}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
|
<td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td>
|
||||||
<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
|
<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
|
||||||
<td>{{FileSize .CalculateBlobSize}}</td>
|
<td>{{FileSize .CalculateBlobSize}}</td>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
<div class="item-main">
|
<div class="item-main">
|
||||||
<div class="item-title">
|
<div class="item-title">
|
||||||
<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
|
<a href="{{.VersionWebLink}}">{{.Package.Name}}</a>
|
||||||
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
|
<span class="label-list">
|
||||||
|
{{template "package/shared/visibility_badge" dict "Package" .Package "Owner" .Owner}}
|
||||||
|
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
{{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}}
|
{{$timeStr := DateUtils.TimeSince .Version.CreatedUnix}}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "packages.versions"}}</strong></p>
|
<p>
|
||||||
|
<a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a>
|
||||||
|
<span class="label-list">
|
||||||
|
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
|
||||||
|
</span>
|
||||||
|
/ <strong>{{ctx.Locale.Tr "packages.versions"}}</strong>
|
||||||
|
</p>
|
||||||
<form class="ui form ignore-dirty">
|
<form class="ui form ignore-dirty">
|
||||||
<div class="ui small fluid action input">
|
<div class="ui small fluid action input">
|
||||||
{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
|
{{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<div class="issue-title-header">
|
<div class="issue-title-header">
|
||||||
{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}
|
{{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}
|
||||||
<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
|
<div class="tw-flex tw-flex-wrap tw-items-center tw-gap-2">
|
||||||
|
<h1 class="tw-mb-0">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
|
||||||
|
<span class="label-list">
|
||||||
|
{{template "package/shared/visibility_badge" dict "Package" .PackageDescriptor.Package "Owner" .PackageDescriptor.Owner}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
|
{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
|
||||||
{{if .HasRepositoryAccess}}
|
{{if .HasRepositoryAccess}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user