Compare commits
1 Commits
main
...
1b62e9c3d4
Author | SHA1 | Date | |
---|---|---|---|
1b62e9c3d4 |
194
gitea-shim/README.md
Normal file
194
gitea-shim/README.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# Gitea GitHub SDK Shim
|
||||||
|
|
||||||
|
A compatibility layer that provides GitHub SDK interfaces (PyGitHub and Octokit) for Gitea repositories. This allows you to use existing GitHub-based code with Gitea instances with minimal changes.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd gitea-shim/python
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd gitea-shim/javascript
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Python (PyGitHub Compatible)
|
||||||
|
|
||||||
|
Instead of using PyGitHub:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Old GitHub code
|
||||||
|
from github import Github
|
||||||
|
gh = Github(token)
|
||||||
|
repo = gh.get_repo("owner/repo")
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the Gitea shim:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# New Gitea code
|
||||||
|
from gitea_github_shim import GiteaGitHubShim
|
||||||
|
gh = GiteaGitHubShim(token, base_url="https://your-gitea-instance.com")
|
||||||
|
repo = gh.get_repo("owner/repo")
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with environment variables:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
os.environ['GITEA_URL'] = 'https://your-gitea-instance.com'
|
||||||
|
|
||||||
|
# Now you can use it just like GitHub
|
||||||
|
gh = GiteaGitHubShim(token) # URL is taken from env
|
||||||
|
repo = gh.get_repo("owner/repo")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/TypeScript (Octokit Compatible)
|
||||||
|
|
||||||
|
Instead of using Octokit:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Old GitHub code
|
||||||
|
const { Octokit } = require("@octokit/rest");
|
||||||
|
const octokit = new Octokit({ auth: token });
|
||||||
|
const repo = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the Gitea shim:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// New Gitea code
|
||||||
|
const { GiteaOctokitShim } = require("gitea-shim");
|
||||||
|
const octokit = new GiteaOctokitShim({
|
||||||
|
auth: token,
|
||||||
|
baseUrl: "https://your-gitea-instance.com"
|
||||||
|
});
|
||||||
|
const repo = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported APIs
|
||||||
|
|
||||||
|
### Repository Operations
|
||||||
|
- `get_repo()` / `rest.repos.get()` - Get repository
|
||||||
|
- `create_repo()` / `rest.repos.createForAuthenticatedUser()` - Create repository
|
||||||
|
- `repo.default_branch` - Get default branch
|
||||||
|
- `repo.create_fork()` / `rest.repos.createFork()` - Fork repository
|
||||||
|
|
||||||
|
### Pull Request Operations
|
||||||
|
- `repo.get_pull()` / `rest.pulls.get()` - Get pull request
|
||||||
|
- `repo.get_pulls()` / `rest.pulls.list()` - List pull requests
|
||||||
|
- `repo.create_pull()` / `rest.pulls.create()` - Create pull request
|
||||||
|
- `pr.update()` / `rest.pulls.update()` - Update pull request
|
||||||
|
- `pr.merge()` / `rest.pulls.merge()` - Merge pull request
|
||||||
|
|
||||||
|
### User Operations
|
||||||
|
- `get_user()` / `rest.users.getAuthenticated()` - Get user
|
||||||
|
- `user.get_repos()` - Get user repositories
|
||||||
|
- `user.add_to_following()` / `rest.users.follow()` - Follow user
|
||||||
|
|
||||||
|
### Activity Operations
|
||||||
|
- `user.get_starred()` / `rest.activity.listReposStarredByUser()` - Get starred repos
|
||||||
|
- `user.get_subscriptions()` / `rest.activity.listWatchedReposForUser()` - Get watched repos
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `GITEA_URL` - Base URL of your Gitea instance (e.g., `https://gitea.example.com`)
|
||||||
|
- `GITEA_TOKEN` - Personal access token for Gitea (you can still use `GITHUB_TOKEN` for compatibility)
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### Step 1: Install the shim
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python
|
||||||
|
pip install gitea-github-shim
|
||||||
|
|
||||||
|
# JavaScript
|
||||||
|
npm install gitea-github-shim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Update imports
|
||||||
|
|
||||||
|
Python:
|
||||||
|
```python
|
||||||
|
# Replace
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
# With
|
||||||
|
from gitea_github_shim import GiteaGitHubShim as Github
|
||||||
|
```
|
||||||
|
|
||||||
|
JavaScript:
|
||||||
|
```javascript
|
||||||
|
// Replace
|
||||||
|
const { Octokit } = require("@octokit/rest");
|
||||||
|
|
||||||
|
// With
|
||||||
|
const { GiteaOctokitShim: Octokit } = require("gitea-github-shim");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Update initialization
|
||||||
|
|
||||||
|
Add the Gitea URL when creating the client:
|
||||||
|
|
||||||
|
Python:
|
||||||
|
```python
|
||||||
|
gh = Github(token, base_url="https://your-gitea-instance.com")
|
||||||
|
```
|
||||||
|
|
||||||
|
JavaScript:
|
||||||
|
```javascript
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: token,
|
||||||
|
baseUrl: "https://your-gitea-instance.com"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Handle API differences
|
||||||
|
|
||||||
|
Some features may not be available or work differently in Gitea:
|
||||||
|
|
||||||
|
1. **Draft PRs** - May not be supported
|
||||||
|
2. **Rate Limiting** - Gitea typically doesn't have rate limits
|
||||||
|
3. **GraphQL API** - Not available in Gitea
|
||||||
|
4. **Advanced Search** - Limited compared to GitHub
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python tests
|
||||||
|
cd gitea-shim/python
|
||||||
|
python -m pytest tests/
|
||||||
|
|
||||||
|
# JavaScript tests
|
||||||
|
cd gitea-shim/javascript
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
1. Check the `todo.md` file for pending tasks
|
||||||
|
2. Add tests for any new functionality
|
||||||
|
3. Update this README with new supported APIs
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Some GitHub-specific features are not available in Gitea
|
||||||
|
- API responses may have slight differences in structure
|
||||||
|
- Not all endpoints are implemented yet
|
||||||
|
- GraphQL is not supported (REST API only)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
216
gitea-shim/python/MIGRATION.md
Normal file
216
gitea-shim/python/MIGRATION.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# Migration Guide for Workflows
|
||||||
|
|
||||||
|
This guide shows how to migrate your workflow files from direct GitHub API usage to the Gitea shim.
|
||||||
|
|
||||||
|
## Quick Migration
|
||||||
|
|
||||||
|
### Step 1: Add Import Path
|
||||||
|
|
||||||
|
Add this to the top of your workflow files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the gitea-shim to the path
|
||||||
|
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
|
||||||
|
sys.path.insert(0, str(gitea_shim_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_shim import get_github_client, is_gitea_mode
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to regular GitHub if shim is not available
|
||||||
|
from github import Github as get_github_client
|
||||||
|
is_gitea_mode = lambda: False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Replace GitHub Client Creation
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
from github import Github
|
||||||
|
gh = Github(os.getenv("GITHUB_TOKEN"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
gh = get_github_client()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Add Logging (Optional)
|
||||||
|
|
||||||
|
Add logging to see which API you're using:
|
||||||
|
|
||||||
|
```python
|
||||||
|
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
### Before Migration
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Task decomposition workflow implementation."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from github import Github
|
||||||
|
import requests
|
||||||
|
from prometheus_swarm.workflows.base import Workflow
|
||||||
|
# ... other imports
|
||||||
|
|
||||||
|
class RepoSummarizerWorkflow(Workflow):
|
||||||
|
def setup(self):
|
||||||
|
"""Set up repository and workspace."""
|
||||||
|
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
|
||||||
|
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
|
||||||
|
|
||||||
|
# Get the default branch from GitHub
|
||||||
|
try:
|
||||||
|
gh = Github(os.getenv("GITHUB_TOKEN"))
|
||||||
|
self.context["repo_full_name"] = (
|
||||||
|
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo = gh.get_repo(
|
||||||
|
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
||||||
|
)
|
||||||
|
self.context["base"] = repo.default_branch
|
||||||
|
log_key_value("Default branch", self.context["base"])
|
||||||
|
except Exception as e:
|
||||||
|
log_error(e, "Failed to get default branch, using 'main'")
|
||||||
|
self.context["base"] = "main"
|
||||||
|
```
|
||||||
|
|
||||||
|
### After Migration
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Task decomposition workflow implementation."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the gitea-shim to the path
|
||||||
|
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
|
||||||
|
sys.path.insert(0, str(gitea_shim_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_shim import get_github_client, is_gitea_mode
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to regular GitHub if shim is not available
|
||||||
|
from github import Github as get_github_client
|
||||||
|
is_gitea_mode = lambda: False
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from prometheus_swarm.workflows.base import Workflow
|
||||||
|
# ... other imports
|
||||||
|
|
||||||
|
class RepoSummarizerWorkflow(Workflow):
|
||||||
|
def setup(self):
|
||||||
|
"""Set up repository and workspace."""
|
||||||
|
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
|
||||||
|
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
|
||||||
|
|
||||||
|
# Get the default branch from GitHub/Gitea
|
||||||
|
try:
|
||||||
|
# Use the shim to get GitHub-compatible client
|
||||||
|
gh = get_github_client()
|
||||||
|
self.context["repo_full_name"] = (
|
||||||
|
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo = gh.get_repo(
|
||||||
|
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
||||||
|
)
|
||||||
|
self.context["base"] = repo.default_branch
|
||||||
|
log_key_value("Default branch", self.context["base"])
|
||||||
|
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
|
||||||
|
except Exception as e:
|
||||||
|
log_error(e, "Failed to get default branch, using 'main'")
|
||||||
|
self.context["base"] = "main"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### For GitHub (Default)
|
||||||
|
```bash
|
||||||
|
export GITHUB_TOKEN="your_github_token"
|
||||||
|
# USE_GITEA is not set or set to 'false'
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Gitea
|
||||||
|
```bash
|
||||||
|
export USE_GITEA="true"
|
||||||
|
export GITEA_URL="http://your-gitea-instance:3000"
|
||||||
|
export GITEA_TOKEN="your_gitea_token" # or use GITHUB_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Migration
|
||||||
|
|
||||||
|
1. **Test with GitHub:**
|
||||||
|
```bash
|
||||||
|
export GITHUB_TOKEN="your_token"
|
||||||
|
python your_workflow.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test with Gitea:**
|
||||||
|
```bash
|
||||||
|
export USE_GITEA="true"
|
||||||
|
export GITEA_URL="http://localhost:3000"
|
||||||
|
export GITHUB_TOKEN="your_token"
|
||||||
|
python your_workflow.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run the shim test:**
|
||||||
|
```bash
|
||||||
|
cd gitea-shim/python
|
||||||
|
python test_shim.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue: Import Error
|
||||||
|
**Error:** `ModuleNotFoundError: No module named 'gitea_shim'`
|
||||||
|
|
||||||
|
**Solution:** Check that the path to the gitea-shim is correct in your import statement.
|
||||||
|
|
||||||
|
### Issue: Gitea Connection Failed
|
||||||
|
**Error:** `Connection refused` or `404 Not Found`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Verify `GITEA_URL` is correct
|
||||||
|
2. Ensure Gitea is running
|
||||||
|
3. Check that your token has the right permissions
|
||||||
|
|
||||||
|
### Issue: Repository Not Found
|
||||||
|
**Error:** `Repository owner/repo not found`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Verify the repository exists in your Gitea instance
|
||||||
|
2. Check that your token has access to the repository
|
||||||
|
3. Ensure the repository name is correct (case-sensitive)
|
||||||
|
|
||||||
|
### Issue: Authentication Failed
|
||||||
|
**Error:** `401 Unauthorized`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Verify your token is valid
|
||||||
|
2. Check token permissions
|
||||||
|
3. Ensure `GITHUB_TOKEN` or `GITEA_TOKEN` is set correctly
|
||||||
|
|
||||||
|
## Benefits of Migration
|
||||||
|
|
||||||
|
1. **Code Reusability:** Same code works with both GitHub and Gitea
|
||||||
|
2. **Easy Testing:** Test against Gitea locally, deploy to GitHub
|
||||||
|
3. **Flexibility:** Switch between APIs without code changes
|
||||||
|
4. **Consistency:** Same interface regardless of backend
|
||||||
|
5. **Future-Proof:** Easy to add support for other Git hosting platforms
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter issues during migration:
|
||||||
|
|
||||||
|
1. Check the test output: `python test_shim.py`
|
||||||
|
2. Verify environment variables are set correctly
|
||||||
|
3. Check the logs for detailed error messages
|
||||||
|
4. Ensure Gitea is running and accessible
|
178
gitea-shim/python/README.md
Normal file
178
gitea-shim/python/README.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Gitea GitHub Shim
|
||||||
|
|
||||||
|
A Python library that provides a GitHub-compatible interface for Gitea, allowing you to use the same code with both GitHub and Gitea APIs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Drop-in replacement** for PyGitHub's `Github` class
|
||||||
|
- **Automatic switching** between GitHub and Gitea based on environment variables
|
||||||
|
- **GitHub-compatible interface** - minimal code changes required
|
||||||
|
- **Error handling** and fallback mechanisms
|
||||||
|
- **Comprehensive model wrappers** for repositories, users, and pull requests
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone this repository or copy the `gitea-shim/python` directory to your project
|
||||||
|
2. Install the required dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The shim automatically detects whether to use GitHub or Gitea based on environment variables:
|
||||||
|
|
||||||
|
### GitHub Mode (Default)
|
||||||
|
```bash
|
||||||
|
export GITHUB_TOKEN="your_github_token"
|
||||||
|
# USE_GITEA is not set or set to 'false'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea Mode
|
||||||
|
```bash
|
||||||
|
export USE_GITEA="true"
|
||||||
|
export GITEA_URL="http://your-gitea-instance:3000"
|
||||||
|
export GITEA_TOKEN="your_gitea_token" # or use GITHUB_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from gitea_shim import get_github_client
|
||||||
|
|
||||||
|
# This will automatically use GitHub or Gitea based on your environment
|
||||||
|
gh = get_github_client()
|
||||||
|
|
||||||
|
# Get a repository (works the same for both APIs)
|
||||||
|
repo = gh.get_repo("owner/repo")
|
||||||
|
|
||||||
|
# Get user information
|
||||||
|
user = gh.get_user()
|
||||||
|
|
||||||
|
# Create a pull request
|
||||||
|
pr = repo.create_pull(
|
||||||
|
title="My PR",
|
||||||
|
body="Description",
|
||||||
|
head="feature-branch",
|
||||||
|
base="main"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration from PyGitHub
|
||||||
|
|
||||||
|
**Before (PyGitHub only):**
|
||||||
|
```python
|
||||||
|
from github import Github
|
||||||
|
|
||||||
|
gh = Github("your_token")
|
||||||
|
repo = gh.get_repo("owner/repo")
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (with shim):**
|
||||||
|
```python
|
||||||
|
from gitea_shim import get_github_client
|
||||||
|
|
||||||
|
gh = get_github_client() # Works with both GitHub and Gitea
|
||||||
|
repo = gh.get_repo("owner/repo") # Same interface!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
from gitea_shim import config, is_gitea_mode, get_api_info
|
||||||
|
|
||||||
|
# Check which API you're using
|
||||||
|
if is_gitea_mode():
|
||||||
|
print("Using Gitea API")
|
||||||
|
else:
|
||||||
|
print("Using GitHub API")
|
||||||
|
|
||||||
|
# Get detailed API information
|
||||||
|
api_info = get_api_info()
|
||||||
|
print(f"API Type: {api_info['type']}")
|
||||||
|
print(f"URL: {api_info['url']}")
|
||||||
|
print(f"Token Set: {api_info['token_set']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Methods
|
||||||
|
|
||||||
|
### Client Methods
|
||||||
|
- `get_repo(full_name)` - Get repository by owner/repo name
|
||||||
|
- `get_user(login=None)` - Get user (authenticated or by login)
|
||||||
|
- `get_organization(login)` - Get organization
|
||||||
|
- `create_repo(name, **kwargs)` - Create new repository
|
||||||
|
- `search_repositories(query, **kwargs)` - Search repositories
|
||||||
|
- `search_users(query, **kwargs)` - Search users
|
||||||
|
- `get_api_status()` - Get API status
|
||||||
|
- `get_rate_limit()` - Get rate limit info
|
||||||
|
- `get_emojis()` - Get available emojis
|
||||||
|
- `get_gitignore_templates()` - Get gitignore templates
|
||||||
|
- `get_license_templates()` - Get license templates
|
||||||
|
|
||||||
|
### Repository Methods
|
||||||
|
- `get_pull(number)` - Get pull request by number
|
||||||
|
- `get_pulls(**kwargs)` - Get list of pull requests
|
||||||
|
- `create_pull(title, body, head, base, **kwargs)` - Create pull request
|
||||||
|
- `get_contents(path, ref=None)` - Get file contents
|
||||||
|
- `get_branches()` - Get all branches
|
||||||
|
- `get_branch(branch)` - Get specific branch
|
||||||
|
- `create_fork(organization=None)` - Fork repository
|
||||||
|
- `get_commits(**kwargs)` - Get commits
|
||||||
|
- `get_commit(sha)` - Get specific commit
|
||||||
|
|
||||||
|
### Pull Request Methods
|
||||||
|
- `update(**kwargs)` - Update pull request
|
||||||
|
- `merge(**kwargs)` - Merge pull request
|
||||||
|
- `get_commits()` - Get commits in PR
|
||||||
|
- `get_files()` - Get files changed in PR
|
||||||
|
- `create_review_comment(body, commit_id, path, position)` - Add review comment
|
||||||
|
- `create_issue_comment(body)` - Add issue comment
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the test suite to verify the shim works correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd gitea-shim/python
|
||||||
|
python test_shim.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `USE_GITEA` | Enable Gitea mode | `false` |
|
||||||
|
| `GITEA_URL` | Gitea instance URL | `http://localhost:3000` |
|
||||||
|
| `GITHUB_TOKEN` | GitHub/Gitea API token | Required |
|
||||||
|
| `GITEA_TOKEN` | Gitea-specific token (optional) | Uses `GITHUB_TOKEN` |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The shim includes comprehensive error handling:
|
||||||
|
|
||||||
|
- **Graceful fallbacks** when Gitea methods don't exist
|
||||||
|
- **Compatible error messages** with GitHub's error format
|
||||||
|
- **Automatic retry logic** for transient failures
|
||||||
|
- **Detailed logging** for debugging
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Some GitHub-specific features may not be available in Gitea
|
||||||
|
- Rate limiting is mocked for Gitea (returns unlimited)
|
||||||
|
- Some advanced search features may differ between APIs
|
||||||
|
- Draft pull requests may not be supported in all Gitea versions
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Add tests for new functionality
|
||||||
|
4. Ensure all tests pass
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
19
gitea-shim/python/__init__.py
Normal file
19
gitea-shim/python/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"""Gitea GitHub Shim - A GitHub-compatible interface for Gitea."""
|
||||||
|
|
||||||
|
from .gitea_github_shim import GiteaGitHubShim
|
||||||
|
from .config import get_github_client, is_gitea_mode, get_api_info, config
|
||||||
|
from .models.repository import Repository
|
||||||
|
from .models.user import User
|
||||||
|
from .models.pull_request import PullRequest
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = [
|
||||||
|
"GiteaGitHubShim",
|
||||||
|
"get_github_client",
|
||||||
|
"is_gitea_mode",
|
||||||
|
"get_api_info",
|
||||||
|
"config",
|
||||||
|
"Repository",
|
||||||
|
"User",
|
||||||
|
"PullRequest"
|
||||||
|
]
|
93
gitea-shim/python/config.py
Normal file
93
gitea-shim/python/config.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
"""Configuration for GitHub/Gitea API switching."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class APIConfig:
|
||||||
|
"""Configuration class for API switching between GitHub and Gitea."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
|
||||||
|
self.gitea_url = os.getenv('GITEA_URL', 'http://localhost:3000')
|
||||||
|
self.github_token = os.getenv('GITHUB_TOKEN')
|
||||||
|
self.gitea_token = os.getenv('GITEA_TOKEN') or self.github_token
|
||||||
|
|
||||||
|
def get_client_class(self):
|
||||||
|
"""Get the appropriate client class based on configuration."""
|
||||||
|
if self.use_gitea:
|
||||||
|
try:
|
||||||
|
from gitea_github_shim import GiteaGitHubShim
|
||||||
|
return GiteaGitHubShim
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to GitHub if Gitea shim is not available
|
||||||
|
from github import Github
|
||||||
|
return Github
|
||||||
|
else:
|
||||||
|
from github import Github
|
||||||
|
return Github
|
||||||
|
|
||||||
|
def get_client_kwargs(self):
|
||||||
|
"""Get the appropriate client initialization arguments."""
|
||||||
|
if self.use_gitea:
|
||||||
|
return {
|
||||||
|
'base_url_or_token': self.gitea_token,
|
||||||
|
'base_url': self.gitea_url
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# For GitHub, we need to pass the token as a positional argument
|
||||||
|
# and return empty kwargs since PyGitHub doesn't use keyword args for token
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_client_args(self):
|
||||||
|
"""Get the positional arguments for client initialization."""
|
||||||
|
if self.use_gitea:
|
||||||
|
# Gitea shim uses keyword arguments
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
# GitHub uses positional argument for token
|
||||||
|
return [self.github_token] if self.github_token else []
|
||||||
|
|
||||||
|
|
||||||
|
# Global configuration instance
|
||||||
|
config = APIConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_client():
|
||||||
|
"""
|
||||||
|
Get a GitHub-compatible client (either real GitHub or Gitea shim).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GitHub-compatible client instance
|
||||||
|
"""
|
||||||
|
client_class = config.get_client_class()
|
||||||
|
args = config.get_client_args()
|
||||||
|
kwargs = config.get_client_kwargs()
|
||||||
|
|
||||||
|
if not config.github_token and not config.gitea_token:
|
||||||
|
raise ValueError(
|
||||||
|
"No API token found. Please set either GITHUB_TOKEN or GITEA_TOKEN environment variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
return client_class(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def is_gitea_mode():
|
||||||
|
"""Check if we're running in Gitea mode."""
|
||||||
|
return config.use_gitea
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_info():
|
||||||
|
"""Get information about the current API configuration."""
|
||||||
|
if config.use_gitea:
|
||||||
|
return {
|
||||||
|
'type': 'gitea',
|
||||||
|
'url': config.gitea_url,
|
||||||
|
'token_set': bool(config.gitea_token)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'type': 'github',
|
||||||
|
'url': 'https://api.github.com',
|
||||||
|
'token_set': bool(config.github_token)
|
||||||
|
}
|
251
gitea-shim/python/gitea_github_shim.py
Normal file
251
gitea-shim/python/gitea_github_shim.py
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
"""Main shim class that provides GitHub-compatible interface for Gitea."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.repository import Repository
|
||||||
|
from models.user import User
|
||||||
|
from models.pull_request import PullRequest
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .models.repository import Repository
|
||||||
|
from .models.user import User
|
||||||
|
from .models.pull_request import PullRequest
|
||||||
|
|
||||||
|
# Mock the Gitea import for testing
|
||||||
|
try:
|
||||||
|
from gitea import Gitea
|
||||||
|
except ImportError:
|
||||||
|
# Mock Gitea for testing purposes
|
||||||
|
class Gitea:
|
||||||
|
def __init__(self, url, token):
|
||||||
|
self.url = url
|
||||||
|
self.token = token
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaGitHubShim:
|
||||||
|
"""
|
||||||
|
A shim class that provides a GitHub-compatible interface for Gitea.
|
||||||
|
|
||||||
|
This class mimics the PyGitHub's Github class API, translating calls
|
||||||
|
to the py-gitea SDK.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_url_or_token: Optional[str] = None,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
timeout: int = 60,
|
||||||
|
per_page: int = 30):
|
||||||
|
"""
|
||||||
|
Initialize the Gitea client with GitHub-compatible interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url_or_token: If only one arg provided, treated as token (GitHub compat)
|
||||||
|
base_url: The Gitea instance URL
|
||||||
|
timeout: Request timeout in seconds
|
||||||
|
per_page: Number of items per page for pagination
|
||||||
|
"""
|
||||||
|
# Handle GitHub-style initialization where only token is provided
|
||||||
|
if base_url is None and base_url_or_token:
|
||||||
|
# In GitHub mode, we expect GITEA_URL to be set
|
||||||
|
self.token = base_url_or_token
|
||||||
|
self.base_url = os.getenv('GITEA_URL', 'http://localhost:3000')
|
||||||
|
else:
|
||||||
|
self.token = base_url_or_token
|
||||||
|
self.base_url = base_url or os.getenv('GITEA_URL', 'http://localhost:3000')
|
||||||
|
|
||||||
|
# Remove trailing slash from base URL
|
||||||
|
self.base_url = self.base_url.rstrip('/')
|
||||||
|
|
||||||
|
# Initialize the Gitea client
|
||||||
|
self._gitea = Gitea(self.base_url, self.token)
|
||||||
|
self.timeout = timeout
|
||||||
|
self.per_page = per_page
|
||||||
|
|
||||||
|
def get_repo(self, full_name_or_id: str) -> Repository:
|
||||||
|
"""
|
||||||
|
Get a repository by its full name (owner/repo) or ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_name_or_id: Repository full name (owner/repo) or ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repository object with GitHub-compatible interface
|
||||||
|
"""
|
||||||
|
if '/' in str(full_name_or_id):
|
||||||
|
# It's a full name (owner/repo)
|
||||||
|
owner, repo_name = full_name_or_id.split('/', 1)
|
||||||
|
try:
|
||||||
|
gitea_repo = self._gitea.get_repo(owner, repo_name)
|
||||||
|
except Exception as e:
|
||||||
|
# Handle case where repo doesn't exist
|
||||||
|
raise Exception(f"Repository {full_name_or_id} not found: {str(e)}")
|
||||||
|
else:
|
||||||
|
# It's an ID - Gitea doesn't support this directly
|
||||||
|
# We'd need to implement a search or listing mechanism
|
||||||
|
raise NotImplementedError("Getting repository by ID is not yet supported")
|
||||||
|
|
||||||
|
return Repository(gitea_repo, self._gitea)
|
||||||
|
|
||||||
|
def get_user(self, login: Optional[str] = None) -> User:
|
||||||
|
"""
|
||||||
|
Get a user by login name or get the authenticated user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
login: Username to get. If None, returns authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User object with GitHub-compatible interface
|
||||||
|
"""
|
||||||
|
if login is None:
|
||||||
|
# Get authenticated user
|
||||||
|
gitea_user = self._gitea.get_user()
|
||||||
|
else:
|
||||||
|
# Get specific user
|
||||||
|
gitea_user = self._gitea.get_user(login)
|
||||||
|
|
||||||
|
return User(gitea_user, self._gitea)
|
||||||
|
|
||||||
|
def get_organization(self, login: str):
|
||||||
|
"""
|
||||||
|
Get an organization by login name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
login: Organization name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Organization object (not yet implemented)
|
||||||
|
"""
|
||||||
|
# Organizations in Gitea are similar to GitHub
|
||||||
|
gitea_org = self._gitea.get_org(login)
|
||||||
|
# TODO: Implement Organization model
|
||||||
|
return gitea_org
|
||||||
|
|
||||||
|
def create_repo(self, name: str, **kwargs) -> Repository:
|
||||||
|
"""
|
||||||
|
Create a new repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Repository name
|
||||||
|
**kwargs: Additional parameters (description, private, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repository object
|
||||||
|
"""
|
||||||
|
# Map GitHub parameters to Gitea parameters
|
||||||
|
gitea_params = {
|
||||||
|
'name': name,
|
||||||
|
'description': kwargs.get('description', ''),
|
||||||
|
'private': kwargs.get('private', False),
|
||||||
|
'auto_init': kwargs.get('auto_init', False),
|
||||||
|
'gitignores': kwargs.get('gitignore_template', ''),
|
||||||
|
'license': kwargs.get('license_template', ''),
|
||||||
|
'readme': kwargs.get('readme', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_repo = self._gitea.create_repo(**gitea_params)
|
||||||
|
return Repository(gitea_repo, self._gitea)
|
||||||
|
|
||||||
|
def get_api_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get API status information."""
|
||||||
|
# Gitea doesn't have a direct equivalent, return version info
|
||||||
|
try:
|
||||||
|
version = self._gitea.get_version()
|
||||||
|
return {
|
||||||
|
'status': 'good',
|
||||||
|
'version': version,
|
||||||
|
'api': 'gitea'
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return {
|
||||||
|
'status': 'unknown',
|
||||||
|
'version': 'unknown',
|
||||||
|
'api': 'gitea'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_rate_limit(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get rate limit information.
|
||||||
|
|
||||||
|
Note: Gitea doesn't have rate limiting like GitHub, so we return
|
||||||
|
mock data indicating no limits.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'rate': {
|
||||||
|
'limit': 999999,
|
||||||
|
'remaining': 999999,
|
||||||
|
'reset': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def search_repositories(self, query: str, **kwargs) -> List[Repository]:
|
||||||
|
"""
|
||||||
|
Search repositories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query
|
||||||
|
**kwargs: Additional search parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Repository objects
|
||||||
|
"""
|
||||||
|
# Gitea search might have different parameters
|
||||||
|
gitea_repos = self._gitea.search_repos(query, **kwargs)
|
||||||
|
return [Repository(repo, self._gitea) for repo in gitea_repos]
|
||||||
|
|
||||||
|
def search_users(self, query: str, **kwargs) -> List[User]:
|
||||||
|
"""
|
||||||
|
Search users.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query
|
||||||
|
**kwargs: Additional search parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of User objects
|
||||||
|
"""
|
||||||
|
gitea_users = self._gitea.search_users(query, **kwargs)
|
||||||
|
return [User(user, self._gitea) for user in gitea_users]
|
||||||
|
|
||||||
|
def get_emojis(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get available emojis.
|
||||||
|
|
||||||
|
Note: Gitea might not support this, return empty dict
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_gitignore_templates(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get available gitignore templates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of template names
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
templates = self._gitea.get_gitignore_templates()
|
||||||
|
return [t.name for t in templates]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_license_templates(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get available license templates.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of license template objects
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
licenses = self._gitea.get_license_templates()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'key': lic.key,
|
||||||
|
'name': lic.name,
|
||||||
|
'url': lic.url,
|
||||||
|
'spdx_id': lic.spdx_id
|
||||||
|
}
|
||||||
|
for lic in licenses
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
return []
|
7
gitea-shim/python/models/__init__.py
Normal file
7
gitea-shim/python/models/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""Model classes for Gitea GitHub shim."""
|
||||||
|
|
||||||
|
from .repository import Repository
|
||||||
|
from .pull_request import PullRequest
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
__all__ = ['Repository', 'PullRequest', 'User']
|
266
gitea-shim/python/models/pull_request.py
Normal file
266
gitea-shim/python/models/pull_request.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
"""Pull Request model that provides GitHub-compatible interface for Gitea pull requests."""
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
|
||||||
|
class PullRequest:
|
||||||
|
"""
|
||||||
|
Pull Request wrapper that provides GitHub-compatible interface for Gitea pull requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, gitea_pr, gitea_repo, gitea_client):
|
||||||
|
"""
|
||||||
|
Initialize pull request wrapper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gitea_pr: The Gitea pull request object
|
||||||
|
gitea_repo: The Gitea repository object
|
||||||
|
gitea_client: The Gitea client instance
|
||||||
|
"""
|
||||||
|
self._pr = gitea_pr
|
||||||
|
self._repo = gitea_repo
|
||||||
|
self._gitea = gitea_client
|
||||||
|
|
||||||
|
# Map Gitea attributes to GitHub attributes
|
||||||
|
self.number = gitea_pr.number
|
||||||
|
self.state = gitea_pr.state
|
||||||
|
self.title = gitea_pr.title
|
||||||
|
self.body = gitea_pr.body
|
||||||
|
self.created_at = gitea_pr.created_at
|
||||||
|
self.updated_at = gitea_pr.updated_at
|
||||||
|
self.closed_at = gitea_pr.closed_at
|
||||||
|
self.merged_at = gitea_pr.merged_at
|
||||||
|
self.merge_commit_sha = gitea_pr.merge_commit_sha
|
||||||
|
self.html_url = gitea_pr.html_url
|
||||||
|
self.diff_url = gitea_pr.diff_url
|
||||||
|
self.patch_url = gitea_pr.patch_url
|
||||||
|
self.mergeable = gitea_pr.mergeable
|
||||||
|
self.merged = gitea_pr.merged
|
||||||
|
self.draft = getattr(gitea_pr, 'draft', False) # Gitea might not have draft PRs
|
||||||
|
|
||||||
|
# User information
|
||||||
|
self.user = self._create_user_object(gitea_pr.user)
|
||||||
|
self.assignee = self._create_user_object(gitea_pr.assignee) if gitea_pr.assignee else None
|
||||||
|
self.assignees = [self._create_user_object(a) for a in getattr(gitea_pr, 'assignees', [])]
|
||||||
|
|
||||||
|
# Branch information
|
||||||
|
self.base = self._create_branch_info(gitea_pr.base)
|
||||||
|
self.head = self._create_branch_info(gitea_pr.head)
|
||||||
|
|
||||||
|
# Labels and milestone
|
||||||
|
self.labels = [self._create_label_object(l) for l in getattr(gitea_pr, 'labels', [])]
|
||||||
|
self.milestone = self._create_milestone_object(gitea_pr.milestone) if gitea_pr.milestone else None
|
||||||
|
|
||||||
|
def _create_user_object(self, gitea_user) -> Dict[str, Any]:
|
||||||
|
"""Create a GitHub-compatible user object."""
|
||||||
|
if not gitea_user:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'login': gitea_user.login,
|
||||||
|
'id': gitea_user.id,
|
||||||
|
'avatar_url': gitea_user.avatar_url,
|
||||||
|
'type': 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_branch_info(self, gitea_branch) -> Dict[str, Any]:
|
||||||
|
"""Create a GitHub-compatible branch info object."""
|
||||||
|
return {
|
||||||
|
'ref': gitea_branch.ref,
|
||||||
|
'sha': gitea_branch.sha,
|
||||||
|
'repo': {
|
||||||
|
'name': gitea_branch.repo.name,
|
||||||
|
'full_name': gitea_branch.repo.full_name,
|
||||||
|
'owner': self._create_user_object(gitea_branch.repo.owner)
|
||||||
|
} if gitea_branch.repo else None
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_label_object(self, gitea_label) -> Dict[str, Any]:
|
||||||
|
"""Create a GitHub-compatible label object."""
|
||||||
|
return {
|
||||||
|
'name': gitea_label.name,
|
||||||
|
'color': gitea_label.color,
|
||||||
|
'description': gitea_label.description
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_milestone_object(self, gitea_milestone) -> Dict[str, Any]:
|
||||||
|
"""Create a GitHub-compatible milestone object."""
|
||||||
|
return {
|
||||||
|
'title': gitea_milestone.title,
|
||||||
|
'description': gitea_milestone.description,
|
||||||
|
'state': gitea_milestone.state,
|
||||||
|
'number': gitea_milestone.id,
|
||||||
|
'due_on': gitea_milestone.due_on
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, **kwargs) -> 'PullRequest':
|
||||||
|
"""
|
||||||
|
Update the pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Fields to update (title, body, state, base, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated PullRequest object
|
||||||
|
"""
|
||||||
|
# Map GitHub parameters to Gitea parameters
|
||||||
|
update_params = {}
|
||||||
|
|
||||||
|
if 'title' in kwargs:
|
||||||
|
update_params['title'] = kwargs['title']
|
||||||
|
if 'body' in kwargs:
|
||||||
|
update_params['body'] = kwargs['body']
|
||||||
|
if 'state' in kwargs:
|
||||||
|
update_params['state'] = kwargs['state']
|
||||||
|
if 'assignee' in kwargs:
|
||||||
|
update_params['assignee'] = kwargs['assignee']
|
||||||
|
if 'assignees' in kwargs:
|
||||||
|
update_params['assignees'] = kwargs['assignees']
|
||||||
|
if 'labels' in kwargs:
|
||||||
|
update_params['labels'] = kwargs['labels']
|
||||||
|
if 'milestone' in kwargs:
|
||||||
|
update_params['milestone'] = kwargs['milestone']
|
||||||
|
|
||||||
|
# Update using Gitea API
|
||||||
|
updated_pr = self._pr.update(**update_params)
|
||||||
|
|
||||||
|
# Return new PullRequest instance with updated data
|
||||||
|
return PullRequest(updated_pr, self._repo, self._gitea)
|
||||||
|
|
||||||
|
def merge(self, commit_title: Optional[str] = None,
|
||||||
|
commit_message: Optional[str] = None,
|
||||||
|
sha: Optional[str] = None,
|
||||||
|
merge_method: str = 'merge') -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Merge the pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
commit_title: Title for the merge commit
|
||||||
|
commit_message: Message for the merge commit
|
||||||
|
sha: SHA that must match the HEAD of the PR
|
||||||
|
merge_method: Merge method ('merge', 'squash', 'rebase')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Merge result information
|
||||||
|
"""
|
||||||
|
# Map GitHub merge methods to Gitea
|
||||||
|
gitea_merge_style = {
|
||||||
|
'merge': 'merge',
|
||||||
|
'squash': 'squash',
|
||||||
|
'rebase': 'rebase'
|
||||||
|
}.get(merge_method, 'merge')
|
||||||
|
|
||||||
|
# Merge using Gitea API
|
||||||
|
result = self._pr.merge(
|
||||||
|
style=gitea_merge_style,
|
||||||
|
title=commit_title,
|
||||||
|
message=commit_message
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sha': result.sha if hasattr(result, 'sha') else None,
|
||||||
|
'merged': True,
|
||||||
|
'message': 'Pull Request successfully merged'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_commits(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get commits in the pull request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of commit objects
|
||||||
|
"""
|
||||||
|
gitea_commits = self._pr.get_commits()
|
||||||
|
|
||||||
|
commits = []
|
||||||
|
for commit in gitea_commits:
|
||||||
|
commits.append({
|
||||||
|
'sha': commit.sha,
|
||||||
|
'commit': {
|
||||||
|
'message': commit.commit.message,
|
||||||
|
'author': {
|
||||||
|
'name': commit.commit.author.name,
|
||||||
|
'email': commit.commit.author.email,
|
||||||
|
'date': commit.commit.author.date
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'html_url': commit.html_url
|
||||||
|
})
|
||||||
|
|
||||||
|
return commits
|
||||||
|
|
||||||
|
def get_files(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get files changed in the pull request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of file objects
|
||||||
|
"""
|
||||||
|
gitea_files = self._pr.get_files()
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for file in gitea_files:
|
||||||
|
files.append({
|
||||||
|
'filename': file.filename,
|
||||||
|
'status': file.status,
|
||||||
|
'additions': file.additions,
|
||||||
|
'deletions': file.deletions,
|
||||||
|
'changes': file.changes,
|
||||||
|
'patch': file.patch,
|
||||||
|
'sha': file.sha,
|
||||||
|
'blob_url': file.blob_url,
|
||||||
|
'raw_url': file.raw_url
|
||||||
|
})
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
def create_review_comment(self, body: str, commit_id: str, path: str,
|
||||||
|
position: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create a review comment on the pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
body: Comment body
|
||||||
|
commit_id: Commit SHA to comment on
|
||||||
|
path: File path to comment on
|
||||||
|
position: Line position in the diff
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comment object
|
||||||
|
"""
|
||||||
|
comment = self._pr.create_review_comment(
|
||||||
|
body=body,
|
||||||
|
commit_id=commit_id,
|
||||||
|
path=path,
|
||||||
|
position=position
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': comment.id,
|
||||||
|
'body': comment.body,
|
||||||
|
'path': comment.path,
|
||||||
|
'position': comment.position,
|
||||||
|
'commit_id': comment.commit_id,
|
||||||
|
'user': self._create_user_object(comment.user),
|
||||||
|
'created_at': comment.created_at,
|
||||||
|
'updated_at': comment.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_issue_comment(self, body: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create a general comment on the pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
body: Comment body
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Comment object
|
||||||
|
"""
|
||||||
|
comment = self._pr.create_comment(body=body)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': comment.id,
|
||||||
|
'body': comment.body,
|
||||||
|
'user': self._create_user_object(comment.user),
|
||||||
|
'created_at': comment.created_at,
|
||||||
|
'updated_at': comment.updated_at
|
||||||
|
}
|
291
gitea-shim/python/models/repository.py
Normal file
291
gitea-shim/python/models/repository.py
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
"""Repository model that provides GitHub-compatible interface for Gitea repositories."""
|
||||||
|
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.pull_request import PullRequest
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .pull_request import PullRequest
|
||||||
|
|
||||||
|
|
||||||
|
class Repository:
|
||||||
|
"""
|
||||||
|
Repository wrapper that provides GitHub-compatible interface for Gitea repositories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, gitea_repo, gitea_client):
|
||||||
|
"""
|
||||||
|
Initialize repository wrapper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gitea_repo: The Gitea repository object
|
||||||
|
gitea_client: The Gitea client instance
|
||||||
|
"""
|
||||||
|
self._repo = gitea_repo
|
||||||
|
self._gitea = gitea_client
|
||||||
|
|
||||||
|
# Map Gitea attributes to GitHub attributes
|
||||||
|
self.name = gitea_repo.name
|
||||||
|
self.full_name = gitea_repo.full_name
|
||||||
|
self.description = gitea_repo.description
|
||||||
|
self.private = gitea_repo.private
|
||||||
|
self.fork = gitea_repo.fork
|
||||||
|
self.created_at = gitea_repo.created_at
|
||||||
|
self.updated_at = gitea_repo.updated_at
|
||||||
|
self.pushed_at = gitea_repo.updated_at # Gitea doesn't have pushed_at
|
||||||
|
self.size = gitea_repo.size
|
||||||
|
self.stargazers_count = gitea_repo.stars_count
|
||||||
|
self.watchers_count = gitea_repo.watchers_count
|
||||||
|
self.forks_count = gitea_repo.forks_count
|
||||||
|
self.open_issues_count = gitea_repo.open_issues_count
|
||||||
|
self.default_branch = gitea_repo.default_branch
|
||||||
|
self.archived = gitea_repo.archived
|
||||||
|
self.html_url = gitea_repo.html_url
|
||||||
|
self.clone_url = gitea_repo.clone_url
|
||||||
|
self.ssh_url = gitea_repo.ssh_url
|
||||||
|
|
||||||
|
# Owner information
|
||||||
|
self.owner = self._create_owner_object(gitea_repo.owner)
|
||||||
|
|
||||||
|
def _create_owner_object(self, gitea_owner) -> Dict[str, Any]:
|
||||||
|
"""Create a GitHub-compatible owner object."""
|
||||||
|
return {
|
||||||
|
'login': gitea_owner.login,
|
||||||
|
'id': gitea_owner.id,
|
||||||
|
'avatar_url': gitea_owner.avatar_url,
|
||||||
|
'type': 'User' if not getattr(gitea_owner, 'is_organization', False) else 'Organization'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pull(self, number: int) -> PullRequest:
|
||||||
|
"""
|
||||||
|
Get a pull request by number.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
number: Pull request number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PullRequest object
|
||||||
|
"""
|
||||||
|
gitea_pr = self._repo.get_pull(number)
|
||||||
|
return PullRequest(gitea_pr, self._repo, self._gitea)
|
||||||
|
|
||||||
|
def get_pulls(self, state: str = 'open', sort: str = 'created',
|
||||||
|
direction: str = 'desc', base: Optional[str] = None,
|
||||||
|
head: Optional[str] = None) -> List[PullRequest]:
|
||||||
|
"""
|
||||||
|
Get pull requests for the repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: State of PRs to return ('open', 'closed', 'all')
|
||||||
|
sort: Sort field ('created', 'updated', 'popularity')
|
||||||
|
direction: Sort direction ('asc', 'desc')
|
||||||
|
base: Filter by base branch
|
||||||
|
head: Filter by head branch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PullRequest objects
|
||||||
|
"""
|
||||||
|
# Map GitHub state to Gitea state
|
||||||
|
gitea_state = state
|
||||||
|
if state == 'all':
|
||||||
|
gitea_state = None # Gitea uses None for all
|
||||||
|
|
||||||
|
gitea_prs = self._repo.get_pulls(state=gitea_state)
|
||||||
|
|
||||||
|
# Convert to PullRequest objects
|
||||||
|
pulls = [PullRequest(pr, self._repo, self._gitea) for pr in gitea_prs]
|
||||||
|
|
||||||
|
# Apply additional filters if needed
|
||||||
|
if base:
|
||||||
|
pulls = [pr for pr in pulls if pr.base.ref == base]
|
||||||
|
if head:
|
||||||
|
pulls = [pr for pr in pulls if pr.head.ref == head]
|
||||||
|
|
||||||
|
return pulls
|
||||||
|
|
||||||
|
def create_pull(self, title: str, body: str, head: str, base: str,
|
||||||
|
draft: bool = False, **kwargs) -> PullRequest:
|
||||||
|
"""
|
||||||
|
Create a new pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: PR title
|
||||||
|
body: PR description
|
||||||
|
head: Source branch
|
||||||
|
base: Target branch
|
||||||
|
draft: Whether PR is a draft
|
||||||
|
**kwargs: Additional parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PullRequest object
|
||||||
|
"""
|
||||||
|
# Create PR using Gitea API
|
||||||
|
gitea_pr = self._repo.create_pull_request(
|
||||||
|
title=title,
|
||||||
|
body=body,
|
||||||
|
head=head,
|
||||||
|
base=base
|
||||||
|
# Note: Gitea might not support draft PRs
|
||||||
|
)
|
||||||
|
|
||||||
|
return PullRequest(gitea_pr, self._repo, self._gitea)
|
||||||
|
|
||||||
|
def get_contents(self, path: str, ref: Optional[str] = None) -> Any:
|
||||||
|
"""
|
||||||
|
Get file contents from repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: File path
|
||||||
|
ref: Git ref (branch, tag, commit SHA)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
File contents object
|
||||||
|
"""
|
||||||
|
# Get file contents using Gitea API
|
||||||
|
contents = self._repo.get_file_contents(path, ref=ref)
|
||||||
|
|
||||||
|
# Create GitHub-compatible response
|
||||||
|
return {
|
||||||
|
'name': contents.name,
|
||||||
|
'path': contents.path,
|
||||||
|
'sha': contents.sha,
|
||||||
|
'size': contents.size,
|
||||||
|
'type': contents.type,
|
||||||
|
'content': contents.content,
|
||||||
|
'encoding': contents.encoding,
|
||||||
|
'download_url': contents.download_url,
|
||||||
|
'html_url': contents.html_url
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_branches(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all branches in the repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of branch objects
|
||||||
|
"""
|
||||||
|
gitea_branches = self._repo.get_branches()
|
||||||
|
|
||||||
|
branches = []
|
||||||
|
for branch in gitea_branches:
|
||||||
|
branches.append({
|
||||||
|
'name': branch.name,
|
||||||
|
'commit': {
|
||||||
|
'sha': branch.commit.id,
|
||||||
|
'url': branch.commit.url
|
||||||
|
},
|
||||||
|
'protected': branch.protected
|
||||||
|
})
|
||||||
|
|
||||||
|
return branches
|
||||||
|
|
||||||
|
def get_branch(self, branch: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get a specific branch.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Branch object
|
||||||
|
"""
|
||||||
|
gitea_branch = self._repo.get_branch(branch)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': gitea_branch.name,
|
||||||
|
'commit': {
|
||||||
|
'sha': gitea_branch.commit.id,
|
||||||
|
'url': gitea_branch.commit.url
|
||||||
|
},
|
||||||
|
'protected': gitea_branch.protected
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_fork(self, organization: Optional[str] = None) -> 'Repository':
|
||||||
|
"""
|
||||||
|
Create a fork of the repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
organization: Organization to fork to (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repository object for the fork
|
||||||
|
"""
|
||||||
|
gitea_fork = self._repo.create_fork(organization=organization)
|
||||||
|
return Repository(gitea_fork, self._gitea)
|
||||||
|
|
||||||
|
def get_commits(self, sha: Optional[str] = None, path: Optional[str] = None,
|
||||||
|
since: Optional[str] = None, until: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get commits from the repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sha: Starting commit SHA or branch
|
||||||
|
path: Only commits containing this path
|
||||||
|
since: Only commits after this date
|
||||||
|
until: Only commits before this date
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of commit objects
|
||||||
|
"""
|
||||||
|
# Note: Gitea API might have different parameters
|
||||||
|
gitea_commits = self._repo.get_commits()
|
||||||
|
|
||||||
|
commits = []
|
||||||
|
for commit in gitea_commits:
|
||||||
|
commits.append({
|
||||||
|
'sha': commit.sha,
|
||||||
|
'commit': {
|
||||||
|
'message': commit.commit.message,
|
||||||
|
'author': {
|
||||||
|
'name': commit.commit.author.name,
|
||||||
|
'email': commit.commit.author.email,
|
||||||
|
'date': commit.commit.author.date
|
||||||
|
},
|
||||||
|
'committer': {
|
||||||
|
'name': commit.commit.committer.name,
|
||||||
|
'email': commit.commit.committer.email,
|
||||||
|
'date': commit.commit.committer.date
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'html_url': commit.html_url,
|
||||||
|
'author': commit.author,
|
||||||
|
'committer': commit.committer
|
||||||
|
})
|
||||||
|
|
||||||
|
return commits
|
||||||
|
|
||||||
|
def get_commit(self, sha: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get a specific commit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sha: Commit SHA
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Commit object
|
||||||
|
"""
|
||||||
|
gitea_commit = self._repo.get_commit(sha)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sha': gitea_commit.sha,
|
||||||
|
'commit': {
|
||||||
|
'message': gitea_commit.commit.message,
|
||||||
|
'author': {
|
||||||
|
'name': gitea_commit.commit.author.name,
|
||||||
|
'email': gitea_commit.commit.author.email,
|
||||||
|
'date': gitea_commit.commit.author.date
|
||||||
|
},
|
||||||
|
'committer': {
|
||||||
|
'name': gitea_commit.commit.committer.name,
|
||||||
|
'email': gitea_commit.commit.committer.email,
|
||||||
|
'date': gitea_commit.commit.committer.date
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'html_url': gitea_commit.html_url,
|
||||||
|
'author': gitea_commit.author,
|
||||||
|
'committer': gitea_commit.committer,
|
||||||
|
'stats': gitea_commit.stats,
|
||||||
|
'files': gitea_commit.files
|
||||||
|
}
|
246
gitea-shim/python/models/user.py
Normal file
246
gitea-shim/python/models/user.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
"""User model that provides GitHub-compatible interface for Gitea users."""
|
||||||
|
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""
|
||||||
|
User wrapper that provides GitHub-compatible interface for Gitea users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, gitea_user, gitea_client):
|
||||||
|
"""
|
||||||
|
Initialize user wrapper.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gitea_user: The Gitea user object
|
||||||
|
gitea_client: The Gitea client instance
|
||||||
|
"""
|
||||||
|
self._user = gitea_user
|
||||||
|
self._gitea = gitea_client
|
||||||
|
|
||||||
|
# Map Gitea attributes to GitHub attributes
|
||||||
|
self.login = gitea_user.login
|
||||||
|
self.id = gitea_user.id
|
||||||
|
self.avatar_url = gitea_user.avatar_url
|
||||||
|
self.html_url = gitea_user.html_url
|
||||||
|
self.type = 'User' # Could be 'User' or 'Organization'
|
||||||
|
self.name = gitea_user.full_name
|
||||||
|
self.company = getattr(gitea_user, 'company', None)
|
||||||
|
self.blog = getattr(gitea_user, 'website', None)
|
||||||
|
self.location = gitea_user.location
|
||||||
|
self.email = gitea_user.email
|
||||||
|
self.bio = getattr(gitea_user, 'description', None)
|
||||||
|
self.public_repos = getattr(gitea_user, 'public_repos', 0)
|
||||||
|
self.followers = getattr(gitea_user, 'followers_count', 0)
|
||||||
|
self.following = getattr(gitea_user, 'following_count', 0)
|
||||||
|
self.created_at = gitea_user.created
|
||||||
|
self.updated_at = getattr(gitea_user, 'last_login', gitea_user.created)
|
||||||
|
|
||||||
|
def get_repos(self, type: str = 'owner', sort: str = 'full_name',
|
||||||
|
direction: str = 'asc') -> List[Any]:
|
||||||
|
"""
|
||||||
|
Get repositories for the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type: Type of repos to return ('all', 'owner', 'member')
|
||||||
|
sort: Sort field ('created', 'updated', 'pushed', 'full_name')
|
||||||
|
direction: Sort direction ('asc', 'desc')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Repository objects
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.repository import Repository
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .repository import Repository
|
||||||
|
|
||||||
|
# Get repos using Gitea API
|
||||||
|
gitea_repos = self._user.get_repos()
|
||||||
|
|
||||||
|
# Convert to Repository objects
|
||||||
|
repos = [Repository(repo, self._gitea) for repo in gitea_repos]
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
if sort == 'full_name':
|
||||||
|
repos.sort(key=lambda r: r.full_name, reverse=(direction == 'desc'))
|
||||||
|
elif sort == 'created':
|
||||||
|
repos.sort(key=lambda r: r.created_at, reverse=(direction == 'desc'))
|
||||||
|
elif sort == 'updated':
|
||||||
|
repos.sort(key=lambda r: r.updated_at, reverse=(direction == 'desc'))
|
||||||
|
|
||||||
|
return repos
|
||||||
|
|
||||||
|
def get_starred(self) -> List[Any]:
|
||||||
|
"""
|
||||||
|
Get repositories starred by the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Repository objects
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.repository import Repository
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .repository import Repository
|
||||||
|
|
||||||
|
# Get starred repos using Gitea API
|
||||||
|
gitea_repos = self._user.get_starred()
|
||||||
|
|
||||||
|
# Convert to Repository objects
|
||||||
|
return [Repository(repo, self._gitea) for repo in gitea_repos]
|
||||||
|
|
||||||
|
def get_subscriptions(self) -> List[Any]:
|
||||||
|
"""
|
||||||
|
Get repositories watched by the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Repository objects
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.repository import Repository
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .repository import Repository
|
||||||
|
|
||||||
|
# Get watched repos using Gitea API
|
||||||
|
gitea_repos = self._user.get_subscriptions()
|
||||||
|
|
||||||
|
# Convert to Repository objects
|
||||||
|
return [Repository(repo, self._gitea) for repo in gitea_repos]
|
||||||
|
|
||||||
|
def get_orgs(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get organizations the user belongs to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of organization objects
|
||||||
|
"""
|
||||||
|
gitea_orgs = self._user.get_orgs()
|
||||||
|
|
||||||
|
orgs = []
|
||||||
|
for org in gitea_orgs:
|
||||||
|
orgs.append({
|
||||||
|
'login': org.username,
|
||||||
|
'id': org.id,
|
||||||
|
'avatar_url': org.avatar_url,
|
||||||
|
'description': org.description,
|
||||||
|
'type': 'Organization'
|
||||||
|
})
|
||||||
|
|
||||||
|
return orgs
|
||||||
|
|
||||||
|
def get_followers(self) -> List['User']:
|
||||||
|
"""
|
||||||
|
Get users following this user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of User objects
|
||||||
|
"""
|
||||||
|
gitea_followers = self._user.get_followers()
|
||||||
|
|
||||||
|
# Convert to User objects
|
||||||
|
return [User(follower, self._gitea) for follower in gitea_followers]
|
||||||
|
|
||||||
|
def get_following(self) -> List['User']:
|
||||||
|
"""
|
||||||
|
Get users this user is following.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of User objects
|
||||||
|
"""
|
||||||
|
gitea_following = self._user.get_following()
|
||||||
|
|
||||||
|
# Convert to User objects
|
||||||
|
return [User(user, self._gitea) for user in gitea_following]
|
||||||
|
|
||||||
|
def has_in_following(self, following: 'User') -> bool:
|
||||||
|
"""
|
||||||
|
Check if this user follows another user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
following: User to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if following, False otherwise
|
||||||
|
"""
|
||||||
|
return self._user.is_following(following.login)
|
||||||
|
|
||||||
|
def add_to_following(self, following: 'User') -> None:
|
||||||
|
"""
|
||||||
|
Follow another user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
following: User to follow
|
||||||
|
"""
|
||||||
|
self._user.follow(following.login)
|
||||||
|
|
||||||
|
def remove_from_following(self, following: 'User') -> None:
|
||||||
|
"""
|
||||||
|
Unfollow another user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
following: User to unfollow
|
||||||
|
"""
|
||||||
|
self._user.unfollow(following.login)
|
||||||
|
|
||||||
|
def get_keys(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get SSH keys for the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of key objects
|
||||||
|
"""
|
||||||
|
gitea_keys = self._user.get_keys()
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
for key in gitea_keys:
|
||||||
|
keys.append({
|
||||||
|
'id': key.id,
|
||||||
|
'key': key.key,
|
||||||
|
'title': key.title,
|
||||||
|
'created_at': key.created_at,
|
||||||
|
'read_only': getattr(key, 'read_only', True)
|
||||||
|
})
|
||||||
|
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def create_repo(self, name: str, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
Create a new repository for the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Repository name
|
||||||
|
**kwargs: Additional parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repository object
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
try:
|
||||||
|
# When running tests or using as a module
|
||||||
|
from models.repository import Repository
|
||||||
|
except ImportError:
|
||||||
|
# When using as a package
|
||||||
|
from .repository import Repository
|
||||||
|
|
||||||
|
# Map GitHub parameters to Gitea parameters
|
||||||
|
gitea_params = {
|
||||||
|
'name': name,
|
||||||
|
'description': kwargs.get('description', ''),
|
||||||
|
'private': kwargs.get('private', False),
|
||||||
|
'auto_init': kwargs.get('auto_init', False),
|
||||||
|
'gitignores': kwargs.get('gitignore_template', ''),
|
||||||
|
'license': kwargs.get('license_template', ''),
|
||||||
|
'readme': kwargs.get('readme', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
gitea_repo = self._user.create_repo(**gitea_params)
|
||||||
|
return Repository(gitea_repo, self._gitea)
|
36
gitea-shim/python/setup.py
Normal file
36
gitea-shim/python/setup.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Setup configuration for gitea-github-shim package."""
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open("../../README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="gitea-github-shim",
|
||||||
|
version="0.1.0",
|
||||||
|
author="Your Name",
|
||||||
|
author_email="your.email@example.com",
|
||||||
|
description="A compatibility layer that provides GitHub SDK interfaces for Gitea",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/yourusername/gitea-github-shim",
|
||||||
|
packages=find_packages(),
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
],
|
||||||
|
python_requires=">=3.6",
|
||||||
|
install_requires=[
|
||||||
|
"py-gitea>=1.16.0",
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
"dev": [
|
||||||
|
"pytest>=6.0",
|
||||||
|
"mock>=4.0",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
51
gitea-shim/python/test_results.md
Normal file
51
gitea-shim/python/test_results.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Python Gitea-GitHub Shim Test Results
|
||||||
|
|
||||||
|
## Test Summary
|
||||||
|
All unit tests for the Python Gitea-GitHub shim are passing successfully!
|
||||||
|
|
||||||
|
### Test Execution
|
||||||
|
```bash
|
||||||
|
$ python3 -m unittest tests.test_gitea_github_shim -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Results: ✅ 17/17 tests passed
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
### GiteaGitHubShim Class (8 tests)
|
||||||
|
- ✅ `test_init_with_token_and_url` - Initialization with both token and URL
|
||||||
|
- ✅ `test_init_with_token_only` - GitHub compatibility mode (token only)
|
||||||
|
- ✅ `test_get_repo` - Getting repository by full name
|
||||||
|
- ✅ `test_get_user_authenticated` - Getting authenticated user
|
||||||
|
- ✅ `test_get_user_by_login` - Getting user by username
|
||||||
|
- ✅ `test_create_repo` - Creating a new repository
|
||||||
|
- ✅ `test_get_api_status` - Getting API status
|
||||||
|
- ✅ `test_get_rate_limit` - Getting rate limit info (mocked)
|
||||||
|
|
||||||
|
### Repository Model (3 tests)
|
||||||
|
- ✅ `test_repository_initialization` - Repository model initialization
|
||||||
|
- ✅ `test_get_pull` - Getting a pull request
|
||||||
|
- ✅ `test_create_pull` - Creating a pull request
|
||||||
|
|
||||||
|
### PullRequest Model (3 tests)
|
||||||
|
- ✅ `test_pull_request_initialization` - PR model initialization
|
||||||
|
- ✅ `test_update_pull_request` - Updating a pull request
|
||||||
|
- ✅ `test_merge_pull_request` - Merging a pull request
|
||||||
|
|
||||||
|
### User Model (3 tests)
|
||||||
|
- ✅ `test_user_initialization` - User model initialization
|
||||||
|
- ✅ `test_get_repos` - Getting user repositories
|
||||||
|
- ✅ `test_follow_user` - Following another user
|
||||||
|
|
||||||
|
## Key Features Tested
|
||||||
|
1. **Drop-in replacement** - The shim can be initialized just like PyGitHub
|
||||||
|
2. **Environment variable support** - GITEA_URL can be used for configuration
|
||||||
|
3. **Model compatibility** - Repository, PullRequest, and User models work as expected
|
||||||
|
4. **API mapping** - Gitea API calls are properly mapped to GitHub-style responses
|
||||||
|
5. **Error handling** - Proper handling of missing attributes with getattr()
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
- Implement JavaScript/TypeScript Octokit shim
|
||||||
|
- Test with actual Gitea instance
|
||||||
|
- Add integration tests
|
||||||
|
- Implement remaining API endpoints (issues, activity, etc.)
|
189
gitea-shim/python/test_shim.py
Normal file
189
gitea-shim/python/test_shim.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test script for the Gitea GitHub Shim."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add current directory to path
|
||||||
|
current_dir = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(current_dir))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_github_shim import GiteaGitHubShim
|
||||||
|
from config import get_github_client, is_gitea_mode, get_api_info
|
||||||
|
from utils import setup_api_environment, log_api_configuration
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Import error: {e}")
|
||||||
|
print("Make sure you're running from the correct directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shim_basic():
|
||||||
|
"""Test basic shim functionality."""
|
||||||
|
print("=== Testing Gitea GitHub Shim ===")
|
||||||
|
|
||||||
|
# Set up environment
|
||||||
|
setup_api_environment()
|
||||||
|
log_api_configuration()
|
||||||
|
|
||||||
|
# Get API info
|
||||||
|
api_info = get_api_info()
|
||||||
|
print(f"API Info: {api_info}")
|
||||||
|
|
||||||
|
# Test client creation
|
||||||
|
try:
|
||||||
|
client = get_github_client()
|
||||||
|
print(f"✓ Successfully created client: {type(client).__name__}")
|
||||||
|
|
||||||
|
# Test API status
|
||||||
|
try:
|
||||||
|
status = client.get_api_status()
|
||||||
|
print(f"✓ API Status: {status}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠ API Status failed: {e}")
|
||||||
|
|
||||||
|
# Test rate limit
|
||||||
|
try:
|
||||||
|
rate_limit = client.get_rate_limit()
|
||||||
|
print(f"✓ Rate Limit: {rate_limit}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠ Rate Limit failed: {e}")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✗ Configuration error: {e}")
|
||||||
|
print(" Please set GITHUB_TOKEN or GITEA_TOKEN environment variable")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to create client: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_repository_access():
|
||||||
|
"""Test repository access functionality."""
|
||||||
|
print("\n=== Testing Repository Access ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_github_client()
|
||||||
|
|
||||||
|
# Test with a known repository (GitHub's hello-world)
|
||||||
|
test_repo = "octocat/Hello-World"
|
||||||
|
|
||||||
|
try:
|
||||||
|
repo = client.get_repo(test_repo)
|
||||||
|
print(f"✓ Successfully accessed repository: {repo.full_name}")
|
||||||
|
print(f" - Default branch: {repo.default_branch}")
|
||||||
|
print(f" - Description: {repo.description}")
|
||||||
|
print(f" - Private: {repo.private}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠ Repository access failed: {e}")
|
||||||
|
print(" This might be expected if using Gitea without the test repo")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✗ Configuration error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Repository test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_access():
|
||||||
|
"""Test user access functionality."""
|
||||||
|
print("\n=== Testing User Access ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_github_client()
|
||||||
|
|
||||||
|
# Test getting authenticated user
|
||||||
|
try:
|
||||||
|
user = client.get_user()
|
||||||
|
print(f"✓ Successfully got authenticated user: {user.login}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠ User access failed: {e}")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✗ Configuration error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ User test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration():
|
||||||
|
"""Test configuration functionality."""
|
||||||
|
print("\n=== Testing Configuration ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test API info
|
||||||
|
api_info = get_api_info()
|
||||||
|
print(f"✓ API Info: {api_info}")
|
||||||
|
|
||||||
|
# Test mode detection
|
||||||
|
mode = "Gitea" if is_gitea_mode() else "GitHub"
|
||||||
|
print(f"✓ API Mode: {mode}")
|
||||||
|
|
||||||
|
# Test client class detection
|
||||||
|
from config import config
|
||||||
|
client_class = config.get_client_class()
|
||||||
|
print(f"✓ Client Class: {client_class.__name__}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Configuration test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main test function."""
|
||||||
|
print("Gitea GitHub Shim Test Suite")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# Check environment
|
||||||
|
print(f"USE_GITEA: {os.getenv('USE_GITEA', 'false')}")
|
||||||
|
print(f"GITEA_URL: {os.getenv('GITEA_URL', 'not set')}")
|
||||||
|
print(f"GITHUB_TOKEN: {'set' if os.getenv('GITHUB_TOKEN') else 'not set'}")
|
||||||
|
print(f"GITEA_TOKEN: {'set' if os.getenv('GITEA_TOKEN') else 'not set'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
tests = [
|
||||||
|
test_configuration,
|
||||||
|
test_shim_basic,
|
||||||
|
test_repository_access,
|
||||||
|
test_user_access,
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
total = len(tests)
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
if test():
|
||||||
|
passed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Test {test.__name__} failed with exception: {e}")
|
||||||
|
|
||||||
|
print(f"\n=== Test Results ===")
|
||||||
|
print(f"Passed: {passed}/{total}")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 All tests passed!")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("⚠ Some tests failed or had warnings")
|
||||||
|
if not os.getenv('GITHUB_TOKEN') and not os.getenv('GITEA_TOKEN'):
|
||||||
|
print("\n💡 To test with real API access, set GITHUB_TOKEN or GITEA_TOKEN")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
1
gitea-shim/python/tests/__init__.py
Normal file
1
gitea-shim/python/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for Gitea GitHub shim."""
|
642
gitea-shim/python/tests/test_gitea_github_shim.py
Normal file
642
gitea-shim/python/tests/test_gitea_github_shim.py
Normal file
@ -0,0 +1,642 @@
|
|||||||
|
"""Unit tests for the Gitea GitHub shim."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, MagicMock, patch
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Import the shim classes
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
from gitea_github_shim import GiteaGitHubShim
|
||||||
|
from models.repository import Repository
|
||||||
|
from models.pull_request import PullRequest
|
||||||
|
from models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class TestGiteaGitHubShim(unittest.TestCase):
|
||||||
|
"""Test cases for GiteaGitHubShim class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.token = "test_token"
|
||||||
|
self.base_url = "https://gitea.example.com"
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_init_with_token_and_url(self, mock_gitea_class):
|
||||||
|
"""Test initialization with both token and URL."""
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
|
||||||
|
self.assertEqual(shim.token, self.token)
|
||||||
|
self.assertEqual(shim.base_url, self.base_url)
|
||||||
|
mock_gitea_class.assert_called_once_with(self.base_url, self.token)
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_init_with_token_only(self, mock_gitea_class):
|
||||||
|
"""Test initialization with only token (GitHub compatibility mode)."""
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {'GITEA_URL': 'https://env.gitea.com'}):
|
||||||
|
shim = GiteaGitHubShim(self.token)
|
||||||
|
|
||||||
|
self.assertEqual(shim.token, self.token)
|
||||||
|
self.assertEqual(shim.base_url, 'https://env.gitea.com')
|
||||||
|
mock_gitea_class.assert_called_once_with('https://env.gitea.com', self.token)
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_get_repo(self, mock_gitea_class):
|
||||||
|
"""Test getting a repository."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
mock_repo = Mock()
|
||||||
|
mock_repo.name = "test-repo"
|
||||||
|
mock_repo.full_name = "owner/test-repo"
|
||||||
|
mock_repo.description = "Test repository"
|
||||||
|
mock_repo.private = False
|
||||||
|
mock_repo.fork = False
|
||||||
|
mock_repo.created_at = datetime.now()
|
||||||
|
mock_repo.updated_at = datetime.now()
|
||||||
|
mock_repo.size = 1024
|
||||||
|
mock_repo.stars_count = 10
|
||||||
|
mock_repo.watchers_count = 5
|
||||||
|
mock_repo.forks_count = 2
|
||||||
|
mock_repo.open_issues_count = 3
|
||||||
|
mock_repo.default_branch = "main"
|
||||||
|
mock_repo.archived = False
|
||||||
|
mock_repo.html_url = "https://gitea.example.com/owner/test-repo"
|
||||||
|
mock_repo.clone_url = "https://gitea.example.com/owner/test-repo.git"
|
||||||
|
mock_repo.ssh_url = "git@gitea.example.com:owner/test-repo.git"
|
||||||
|
|
||||||
|
mock_owner = Mock()
|
||||||
|
mock_owner.login = "owner"
|
||||||
|
mock_owner.id = 1
|
||||||
|
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
mock_repo.owner = mock_owner
|
||||||
|
|
||||||
|
mock_gitea.get_repo.return_value = mock_repo
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
repo = shim.get_repo("owner/test-repo")
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(repo, Repository)
|
||||||
|
self.assertEqual(repo.name, "test-repo")
|
||||||
|
self.assertEqual(repo.full_name, "owner/test-repo")
|
||||||
|
self.assertEqual(repo.default_branch, "main")
|
||||||
|
mock_gitea.get_repo.assert_called_once_with("owner", "test-repo")
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_get_user_authenticated(self, mock_gitea_class):
|
||||||
|
"""Test getting authenticated user."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
mock_user = Mock()
|
||||||
|
mock_user.login = "testuser"
|
||||||
|
mock_user.id = 1
|
||||||
|
mock_user.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
mock_user.html_url = "https://gitea.example.com/testuser"
|
||||||
|
mock_user.full_name = "Test User"
|
||||||
|
mock_user.location = "Earth"
|
||||||
|
mock_user.email = "test@example.com"
|
||||||
|
mock_user.created = datetime.now()
|
||||||
|
|
||||||
|
mock_gitea.get_user.return_value = mock_user
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
user = shim.get_user()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(user, User)
|
||||||
|
self.assertEqual(user.login, "testuser")
|
||||||
|
self.assertEqual(user.email, "test@example.com")
|
||||||
|
mock_gitea.get_user.assert_called_once_with()
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_get_user_by_login(self, mock_gitea_class):
|
||||||
|
"""Test getting user by login name."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
mock_user = Mock()
|
||||||
|
mock_user.login = "otheruser"
|
||||||
|
mock_user.id = 2
|
||||||
|
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
|
||||||
|
mock_user.html_url = "https://gitea.example.com/otheruser"
|
||||||
|
mock_user.full_name = "Other User"
|
||||||
|
mock_user.location = "Mars"
|
||||||
|
mock_user.email = "other@example.com"
|
||||||
|
mock_user.created = datetime.now()
|
||||||
|
|
||||||
|
mock_gitea.get_user.return_value = mock_user
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
user = shim.get_user("otheruser")
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(user, User)
|
||||||
|
self.assertEqual(user.login, "otheruser")
|
||||||
|
mock_gitea.get_user.assert_called_once_with("otheruser")
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_create_repo(self, mock_gitea_class):
|
||||||
|
"""Test creating a repository."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
mock_repo = Mock()
|
||||||
|
mock_repo.name = "new-repo"
|
||||||
|
mock_repo.full_name = "testuser/new-repo"
|
||||||
|
mock_repo.description = "New test repository"
|
||||||
|
mock_repo.private = True
|
||||||
|
mock_repo.fork = False
|
||||||
|
mock_repo.created_at = datetime.now()
|
||||||
|
mock_repo.updated_at = datetime.now()
|
||||||
|
mock_repo.size = 0
|
||||||
|
mock_repo.stars_count = 0
|
||||||
|
mock_repo.watchers_count = 0
|
||||||
|
mock_repo.forks_count = 0
|
||||||
|
mock_repo.open_issues_count = 0
|
||||||
|
mock_repo.default_branch = "main"
|
||||||
|
mock_repo.archived = False
|
||||||
|
mock_repo.html_url = "https://gitea.example.com/testuser/new-repo"
|
||||||
|
mock_repo.clone_url = "https://gitea.example.com/testuser/new-repo.git"
|
||||||
|
mock_repo.ssh_url = "git@gitea.example.com:testuser/new-repo.git"
|
||||||
|
|
||||||
|
mock_owner = Mock()
|
||||||
|
mock_owner.login = "testuser"
|
||||||
|
mock_owner.id = 1
|
||||||
|
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
mock_repo.owner = mock_owner
|
||||||
|
|
||||||
|
mock_gitea.create_repo.return_value = mock_repo
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
repo = shim.create_repo(
|
||||||
|
"new-repo",
|
||||||
|
description="New test repository",
|
||||||
|
private=True,
|
||||||
|
auto_init=True,
|
||||||
|
gitignore_template="Python",
|
||||||
|
license_template="MIT"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(repo, Repository)
|
||||||
|
self.assertEqual(repo.name, "new-repo")
|
||||||
|
self.assertEqual(repo.description, "New test repository")
|
||||||
|
self.assertTrue(repo.private)
|
||||||
|
|
||||||
|
mock_gitea.create_repo.assert_called_once_with(
|
||||||
|
name="new-repo",
|
||||||
|
description="New test repository",
|
||||||
|
private=True,
|
||||||
|
auto_init=True,
|
||||||
|
gitignores="Python",
|
||||||
|
license="MIT",
|
||||||
|
readme=""
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_get_api_status(self, mock_gitea_class):
|
||||||
|
"""Test getting API status."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
mock_gitea.get_version.return_value = "1.17.0"
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
status = shim.get_api_status()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(status['status'], 'good')
|
||||||
|
self.assertEqual(status['version'], '1.17.0')
|
||||||
|
self.assertEqual(status['api'], 'gitea')
|
||||||
|
|
||||||
|
@patch('gitea_github_shim.Gitea')
|
||||||
|
def test_get_rate_limit(self, mock_gitea_class):
|
||||||
|
"""Test getting rate limit (mock for Gitea)."""
|
||||||
|
# Set up mocks
|
||||||
|
mock_gitea = Mock()
|
||||||
|
mock_gitea_class.return_value = mock_gitea
|
||||||
|
|
||||||
|
# Test
|
||||||
|
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
|
||||||
|
rate_limit = shim.get_rate_limit()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(rate_limit['rate']['limit'], 999999)
|
||||||
|
self.assertEqual(rate_limit['rate']['remaining'], 999999)
|
||||||
|
self.assertEqual(rate_limit['rate']['reset'], 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepository(unittest.TestCase):
|
||||||
|
"""Test cases for Repository model."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_gitea = Mock()
|
||||||
|
self.mock_repo = Mock()
|
||||||
|
|
||||||
|
# Set up repository attributes
|
||||||
|
self.mock_repo.name = "test-repo"
|
||||||
|
self.mock_repo.full_name = "owner/test-repo"
|
||||||
|
self.mock_repo.description = "Test repository"
|
||||||
|
self.mock_repo.private = False
|
||||||
|
self.mock_repo.fork = False
|
||||||
|
self.mock_repo.created_at = datetime.now()
|
||||||
|
self.mock_repo.updated_at = datetime.now()
|
||||||
|
self.mock_repo.size = 1024
|
||||||
|
self.mock_repo.stars_count = 10
|
||||||
|
self.mock_repo.watchers_count = 5
|
||||||
|
self.mock_repo.forks_count = 2
|
||||||
|
self.mock_repo.open_issues_count = 3
|
||||||
|
self.mock_repo.default_branch = "main"
|
||||||
|
self.mock_repo.archived = False
|
||||||
|
self.mock_repo.html_url = "https://gitea.example.com/owner/test-repo"
|
||||||
|
self.mock_repo.clone_url = "https://gitea.example.com/owner/test-repo.git"
|
||||||
|
self.mock_repo.ssh_url = "git@gitea.example.com:owner/test-repo.git"
|
||||||
|
|
||||||
|
mock_owner = Mock()
|
||||||
|
mock_owner.login = "owner"
|
||||||
|
mock_owner.id = 1
|
||||||
|
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
self.mock_repo.owner = mock_owner
|
||||||
|
|
||||||
|
def test_repository_initialization(self):
|
||||||
|
"""Test Repository model initialization."""
|
||||||
|
repo = Repository(self.mock_repo, self.mock_gitea)
|
||||||
|
|
||||||
|
self.assertEqual(repo.name, "test-repo")
|
||||||
|
self.assertEqual(repo.full_name, "owner/test-repo")
|
||||||
|
self.assertEqual(repo.default_branch, "main")
|
||||||
|
self.assertEqual(repo.stargazers_count, 10)
|
||||||
|
self.assertEqual(repo.owner['login'], "owner")
|
||||||
|
|
||||||
|
def test_get_pull(self):
|
||||||
|
"""Test getting a pull request."""
|
||||||
|
# Set up mock PR
|
||||||
|
mock_pr = Mock()
|
||||||
|
mock_pr.number = 1
|
||||||
|
mock_pr.state = "open"
|
||||||
|
mock_pr.title = "Test PR"
|
||||||
|
mock_pr.body = "Test PR body"
|
||||||
|
mock_pr.created_at = datetime.now()
|
||||||
|
mock_pr.updated_at = datetime.now()
|
||||||
|
mock_pr.closed_at = None
|
||||||
|
mock_pr.merged_at = None
|
||||||
|
mock_pr.merge_commit_sha = None
|
||||||
|
mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/1"
|
||||||
|
mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/1.diff"
|
||||||
|
mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/1.patch"
|
||||||
|
mock_pr.mergeable = True
|
||||||
|
mock_pr.merged = False
|
||||||
|
|
||||||
|
mock_user = Mock()
|
||||||
|
mock_user.login = "contributor"
|
||||||
|
mock_user.id = 2
|
||||||
|
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
|
||||||
|
mock_pr.user = mock_user
|
||||||
|
mock_pr.assignee = None
|
||||||
|
mock_pr.assignees = [] # Empty list of assignees
|
||||||
|
mock_pr.labels = [] # Empty list of labels
|
||||||
|
|
||||||
|
mock_base = Mock()
|
||||||
|
mock_base.ref = "main"
|
||||||
|
mock_base.sha = "abc123"
|
||||||
|
mock_base.repo = self.mock_repo
|
||||||
|
mock_pr.base = mock_base
|
||||||
|
|
||||||
|
mock_head = Mock()
|
||||||
|
mock_head.ref = "feature-branch"
|
||||||
|
mock_head.sha = "def456"
|
||||||
|
mock_head.repo = self.mock_repo
|
||||||
|
mock_pr.head = mock_head
|
||||||
|
|
||||||
|
mock_pr.milestone = None
|
||||||
|
|
||||||
|
self.mock_repo.get_pull.return_value = mock_pr
|
||||||
|
|
||||||
|
# Test
|
||||||
|
repo = Repository(self.mock_repo, self.mock_gitea)
|
||||||
|
pr = repo.get_pull(1)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(pr, PullRequest)
|
||||||
|
self.assertEqual(pr.number, 1)
|
||||||
|
self.assertEqual(pr.title, "Test PR")
|
||||||
|
self.assertEqual(pr.state, "open")
|
||||||
|
self.mock_repo.get_pull.assert_called_once_with(1)
|
||||||
|
|
||||||
|
def test_create_pull(self):
|
||||||
|
"""Test creating a pull request."""
|
||||||
|
# Set up mock PR
|
||||||
|
mock_pr = Mock()
|
||||||
|
mock_pr.number = 2
|
||||||
|
mock_pr.state = "open"
|
||||||
|
mock_pr.title = "New feature"
|
||||||
|
mock_pr.body = "This adds a new feature"
|
||||||
|
mock_pr.created_at = datetime.now()
|
||||||
|
mock_pr.updated_at = datetime.now()
|
||||||
|
mock_pr.closed_at = None
|
||||||
|
mock_pr.merged_at = None
|
||||||
|
mock_pr.merge_commit_sha = None
|
||||||
|
mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/2"
|
||||||
|
mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/2.diff"
|
||||||
|
mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/2.patch"
|
||||||
|
mock_pr.mergeable = True
|
||||||
|
mock_pr.merged = False
|
||||||
|
|
||||||
|
mock_user = Mock()
|
||||||
|
mock_user.login = "contributor"
|
||||||
|
mock_user.id = 2
|
||||||
|
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
|
||||||
|
mock_pr.user = mock_user
|
||||||
|
mock_pr.assignee = None
|
||||||
|
mock_pr.assignees = [] # Empty list of assignees
|
||||||
|
mock_pr.labels = [] # Empty list of labels
|
||||||
|
|
||||||
|
mock_base = Mock()
|
||||||
|
mock_base.ref = "main"
|
||||||
|
mock_base.sha = "abc123"
|
||||||
|
mock_base.repo = self.mock_repo
|
||||||
|
mock_pr.base = mock_base
|
||||||
|
|
||||||
|
mock_head = Mock()
|
||||||
|
mock_head.ref = "feature/new-feature"
|
||||||
|
mock_head.sha = "ghi789"
|
||||||
|
mock_head.repo = self.mock_repo
|
||||||
|
mock_pr.head = mock_head
|
||||||
|
|
||||||
|
mock_pr.milestone = None
|
||||||
|
|
||||||
|
self.mock_repo.create_pull_request.return_value = mock_pr
|
||||||
|
|
||||||
|
# Test
|
||||||
|
repo = Repository(self.mock_repo, self.mock_gitea)
|
||||||
|
pr = repo.create_pull(
|
||||||
|
title="New feature",
|
||||||
|
body="This adds a new feature",
|
||||||
|
head="feature/new-feature",
|
||||||
|
base="main"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertIsInstance(pr, PullRequest)
|
||||||
|
self.assertEqual(pr.title, "New feature")
|
||||||
|
self.assertEqual(pr.base['ref'], "main")
|
||||||
|
self.assertEqual(pr.head['ref'], "feature/new-feature")
|
||||||
|
|
||||||
|
self.mock_repo.create_pull_request.assert_called_once_with(
|
||||||
|
title="New feature",
|
||||||
|
body="This adds a new feature",
|
||||||
|
head="feature/new-feature",
|
||||||
|
base="main"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPullRequest(unittest.TestCase):
|
||||||
|
"""Test cases for PullRequest model."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_gitea = Mock()
|
||||||
|
self.mock_repo = Mock()
|
||||||
|
self.mock_pr = Mock()
|
||||||
|
|
||||||
|
# Set up PR attributes
|
||||||
|
self.mock_pr.number = 1
|
||||||
|
self.mock_pr.state = "open"
|
||||||
|
self.mock_pr.title = "Test PR"
|
||||||
|
self.mock_pr.body = "Test PR body"
|
||||||
|
self.mock_pr.created_at = datetime.now()
|
||||||
|
self.mock_pr.updated_at = datetime.now()
|
||||||
|
self.mock_pr.closed_at = None
|
||||||
|
self.mock_pr.merged_at = None
|
||||||
|
self.mock_pr.merge_commit_sha = None
|
||||||
|
self.mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/1"
|
||||||
|
self.mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/1.diff"
|
||||||
|
self.mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/1.patch"
|
||||||
|
self.mock_pr.mergeable = True
|
||||||
|
self.mock_pr.merged = False
|
||||||
|
|
||||||
|
mock_user = Mock()
|
||||||
|
mock_user.login = "contributor"
|
||||||
|
mock_user.id = 2
|
||||||
|
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
|
||||||
|
self.mock_pr.user = mock_user
|
||||||
|
self.mock_pr.assignee = None
|
||||||
|
|
||||||
|
# Add missing attributes
|
||||||
|
self.mock_pr.assignees = [] # Empty list of assignees
|
||||||
|
self.mock_pr.labels = [] # Empty list of labels
|
||||||
|
|
||||||
|
mock_base = Mock()
|
||||||
|
mock_base.ref = "main"
|
||||||
|
mock_base.sha = "abc123"
|
||||||
|
mock_base.repo = Mock()
|
||||||
|
mock_base.repo.name = "test-repo"
|
||||||
|
mock_base.repo.full_name = "owner/test-repo"
|
||||||
|
mock_base.repo.owner = Mock()
|
||||||
|
mock_base.repo.owner.login = "owner"
|
||||||
|
mock_base.repo.owner.id = 1
|
||||||
|
mock_base.repo.owner.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
self.mock_pr.base = mock_base
|
||||||
|
|
||||||
|
mock_head = Mock()
|
||||||
|
mock_head.ref = "feature-branch"
|
||||||
|
mock_head.sha = "def456"
|
||||||
|
mock_head.repo = Mock()
|
||||||
|
mock_head.repo.name = "test-repo"
|
||||||
|
mock_head.repo.full_name = "contributor/test-repo"
|
||||||
|
mock_head.repo.owner = mock_user
|
||||||
|
self.mock_pr.head = mock_head
|
||||||
|
|
||||||
|
self.mock_pr.milestone = None
|
||||||
|
|
||||||
|
def test_pull_request_initialization(self):
|
||||||
|
"""Test PullRequest model initialization."""
|
||||||
|
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
|
||||||
|
|
||||||
|
self.assertEqual(pr.number, 1)
|
||||||
|
self.assertEqual(pr.title, "Test PR")
|
||||||
|
self.assertEqual(pr.state, "open")
|
||||||
|
self.assertFalse(pr.merged)
|
||||||
|
self.assertEqual(pr.user['login'], "contributor")
|
||||||
|
self.assertEqual(pr.base['ref'], "main")
|
||||||
|
self.assertEqual(pr.head['ref'], "feature-branch")
|
||||||
|
|
||||||
|
def test_update_pull_request(self):
|
||||||
|
"""Test updating a pull request."""
|
||||||
|
# Set up mock for update
|
||||||
|
updated_pr = Mock()
|
||||||
|
updated_pr.number = 1
|
||||||
|
updated_pr.state = "open"
|
||||||
|
updated_pr.title = "Updated PR Title"
|
||||||
|
updated_pr.body = "Updated PR body"
|
||||||
|
updated_pr.created_at = self.mock_pr.created_at
|
||||||
|
updated_pr.updated_at = datetime.now()
|
||||||
|
updated_pr.closed_at = None
|
||||||
|
updated_pr.merged_at = None
|
||||||
|
updated_pr.merge_commit_sha = None
|
||||||
|
updated_pr.html_url = self.mock_pr.html_url
|
||||||
|
updated_pr.diff_url = self.mock_pr.diff_url
|
||||||
|
updated_pr.patch_url = self.mock_pr.patch_url
|
||||||
|
updated_pr.mergeable = True
|
||||||
|
updated_pr.merged = False
|
||||||
|
updated_pr.user = self.mock_pr.user
|
||||||
|
updated_pr.assignee = None
|
||||||
|
updated_pr.base = self.mock_pr.base
|
||||||
|
updated_pr.head = self.mock_pr.head
|
||||||
|
updated_pr.milestone = None
|
||||||
|
|
||||||
|
# Add missing attributes
|
||||||
|
updated_pr.assignees = [] # Empty list of assignees
|
||||||
|
updated_pr.labels = [] # Empty list of labels
|
||||||
|
|
||||||
|
self.mock_pr.update.return_value = updated_pr
|
||||||
|
|
||||||
|
# Test
|
||||||
|
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
|
||||||
|
updated = pr.update(title="Updated PR Title", body="Updated PR body")
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(updated.title, "Updated PR Title")
|
||||||
|
self.assertEqual(updated.body, "Updated PR body")
|
||||||
|
self.mock_pr.update.assert_called_once_with(
|
||||||
|
title="Updated PR Title",
|
||||||
|
body="Updated PR body"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_merge_pull_request(self):
|
||||||
|
"""Test merging a pull request."""
|
||||||
|
# Set up mock for merge
|
||||||
|
merge_result = Mock()
|
||||||
|
merge_result.sha = "merged123"
|
||||||
|
|
||||||
|
self.mock_pr.merge.return_value = merge_result
|
||||||
|
|
||||||
|
# Test
|
||||||
|
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
|
||||||
|
result = pr.merge(
|
||||||
|
commit_title="Merge PR #1",
|
||||||
|
commit_message="This merges the feature",
|
||||||
|
merge_method="squash"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(result['sha'], "merged123")
|
||||||
|
self.assertTrue(result['merged'])
|
||||||
|
self.assertEqual(result['message'], "Pull Request successfully merged")
|
||||||
|
|
||||||
|
self.mock_pr.merge.assert_called_once_with(
|
||||||
|
style="squash",
|
||||||
|
title="Merge PR #1",
|
||||||
|
message="This merges the feature"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUser(unittest.TestCase):
|
||||||
|
"""Test cases for User model."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_gitea = Mock()
|
||||||
|
self.mock_user = Mock()
|
||||||
|
|
||||||
|
# Set up user attributes
|
||||||
|
self.mock_user.login = "testuser"
|
||||||
|
self.mock_user.id = 1
|
||||||
|
self.mock_user.avatar_url = "https://gitea.example.com/avatars/1"
|
||||||
|
self.mock_user.html_url = "https://gitea.example.com/testuser"
|
||||||
|
self.mock_user.full_name = "Test User"
|
||||||
|
self.mock_user.location = "Earth"
|
||||||
|
self.mock_user.email = "test@example.com"
|
||||||
|
self.mock_user.created = datetime.now()
|
||||||
|
|
||||||
|
def test_user_initialization(self):
|
||||||
|
"""Test User model initialization."""
|
||||||
|
user = User(self.mock_user, self.mock_gitea)
|
||||||
|
|
||||||
|
self.assertEqual(user.login, "testuser")
|
||||||
|
self.assertEqual(user.name, "Test User")
|
||||||
|
self.assertEqual(user.email, "test@example.com")
|
||||||
|
self.assertEqual(user.location, "Earth")
|
||||||
|
self.assertEqual(user.type, "User")
|
||||||
|
|
||||||
|
def test_get_repos(self):
|
||||||
|
"""Test getting user repositories."""
|
||||||
|
# Set up mock repos
|
||||||
|
mock_repo1 = Mock()
|
||||||
|
mock_repo1.name = "repo1"
|
||||||
|
mock_repo1.full_name = "testuser/repo1"
|
||||||
|
mock_repo1.created_at = datetime.now()
|
||||||
|
mock_repo1.updated_at = datetime.now()
|
||||||
|
|
||||||
|
mock_repo2 = Mock()
|
||||||
|
mock_repo2.name = "repo2"
|
||||||
|
mock_repo2.full_name = "testuser/repo2"
|
||||||
|
mock_repo2.created_at = datetime.now()
|
||||||
|
mock_repo2.updated_at = datetime.now()
|
||||||
|
|
||||||
|
# Set other required attributes for Repository initialization
|
||||||
|
for repo in [mock_repo1, mock_repo2]:
|
||||||
|
repo.description = "Test repo"
|
||||||
|
repo.private = False
|
||||||
|
repo.fork = False
|
||||||
|
repo.size = 100
|
||||||
|
repo.stars_count = 1
|
||||||
|
repo.watchers_count = 1
|
||||||
|
repo.forks_count = 0
|
||||||
|
repo.open_issues_count = 0
|
||||||
|
repo.default_branch = "main"
|
||||||
|
repo.archived = False
|
||||||
|
repo.html_url = f"https://gitea.example.com/{repo.full_name}"
|
||||||
|
repo.clone_url = f"https://gitea.example.com/{repo.full_name}.git"
|
||||||
|
repo.ssh_url = f"git@gitea.example.com:{repo.full_name}.git"
|
||||||
|
repo.owner = self.mock_user
|
||||||
|
|
||||||
|
self.mock_user.get_repos.return_value = [mock_repo1, mock_repo2]
|
||||||
|
|
||||||
|
# Test
|
||||||
|
user = User(self.mock_user, self.mock_gitea)
|
||||||
|
repos = user.get_repos()
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.assertEqual(len(repos), 2)
|
||||||
|
self.assertEqual(repos[0].name, "repo1")
|
||||||
|
self.assertEqual(repos[1].name, "repo2")
|
||||||
|
self.mock_user.get_repos.assert_called_once()
|
||||||
|
|
||||||
|
def test_follow_user(self):
|
||||||
|
"""Test following another user."""
|
||||||
|
# Create another user to follow
|
||||||
|
other_user = Mock()
|
||||||
|
other_user.login = "otheruser"
|
||||||
|
|
||||||
|
# Test
|
||||||
|
user = User(self.mock_user, self.mock_gitea)
|
||||||
|
other = User(other_user, self.mock_gitea)
|
||||||
|
|
||||||
|
user.add_to_following(other)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
self.mock_user.follow.assert_called_once_with("otheruser")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
121
gitea-shim/python/utils.py
Normal file
121
gitea-shim/python/utils.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""Utility functions for GitHub/Gitea migration."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_api_environment():
|
||||||
|
"""
|
||||||
|
Set up environment variables for API configuration.
|
||||||
|
|
||||||
|
This function helps configure the environment for either GitHub or Gitea mode.
|
||||||
|
"""
|
||||||
|
# Check if we should use Gitea
|
||||||
|
use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
if use_gitea:
|
||||||
|
logger.info("Configuring for Gitea mode")
|
||||||
|
|
||||||
|
# Ensure GITEA_URL is set
|
||||||
|
if not os.getenv('GITEA_URL'):
|
||||||
|
os.environ['GITEA_URL'] = 'http://localhost:3000'
|
||||||
|
logger.info(f"Set GITEA_URL to {os.environ['GITEA_URL']}")
|
||||||
|
|
||||||
|
# Use GITEA_TOKEN if available, otherwise fall back to GITHUB_TOKEN
|
||||||
|
if not os.getenv('GITEA_TOKEN') and os.getenv('GITHUB_TOKEN'):
|
||||||
|
os.environ['GITEA_TOKEN'] = os.environ['GITHUB_TOKEN']
|
||||||
|
logger.info("Using GITHUB_TOKEN as GITEA_TOKEN")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.info("Configuring for GitHub mode")
|
||||||
|
|
||||||
|
# Ensure GITHUB_TOKEN is set
|
||||||
|
if not os.getenv('GITHUB_TOKEN'):
|
||||||
|
logger.warning("GITHUB_TOKEN not set")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_api_configuration() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate the current API configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with validation results
|
||||||
|
"""
|
||||||
|
use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
if use_gitea:
|
||||||
|
gitea_url = os.getenv('GITEA_URL')
|
||||||
|
gitea_token = os.getenv('GITEA_TOKEN') or os.getenv('GITHUB_TOKEN')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'mode': 'gitea',
|
||||||
|
'url_set': bool(gitea_url),
|
||||||
|
'token_set': bool(gitea_token),
|
||||||
|
'url': gitea_url,
|
||||||
|
'valid': bool(gitea_url and gitea_token)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
github_token = os.getenv('GITHUB_TOKEN')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'mode': 'github',
|
||||||
|
'url_set': True, # GitHub URL is fixed
|
||||||
|
'token_set': bool(github_token),
|
||||||
|
'url': 'https://api.github.com',
|
||||||
|
'valid': bool(github_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_github_imports():
|
||||||
|
"""
|
||||||
|
Helper function to show how to migrate GitHub imports.
|
||||||
|
|
||||||
|
This function doesn't actually do anything, but serves as documentation
|
||||||
|
for the migration process.
|
||||||
|
"""
|
||||||
|
migration_examples = {
|
||||||
|
'old_import': 'from github import Github',
|
||||||
|
'new_import': 'from gitea_shim import get_github_client',
|
||||||
|
'old_usage': 'gh = Github(token)',
|
||||||
|
'new_usage': 'gh = get_github_client()',
|
||||||
|
'old_repo': 'repo = gh.get_repo("owner/repo")',
|
||||||
|
'new_repo': 'repo = gh.get_repo("owner/repo") # Same interface!'
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Migration examples:")
|
||||||
|
for key, value in migration_examples.items():
|
||||||
|
logger.info(f"{key}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_client_info() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get information about the current API client configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with API client information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from .config import get_api_info
|
||||||
|
return get_api_info()
|
||||||
|
except ImportError:
|
||||||
|
return validate_api_configuration()
|
||||||
|
|
||||||
|
|
||||||
|
def log_api_configuration():
|
||||||
|
"""Log the current API configuration for debugging."""
|
||||||
|
config = get_api_client_info()
|
||||||
|
logger.info(f"API Configuration: {config}")
|
||||||
|
|
||||||
|
if not config.get('valid', False):
|
||||||
|
logger.warning("API configuration is invalid!")
|
||||||
|
if config['mode'] == 'gitea':
|
||||||
|
if not config['url_set']:
|
||||||
|
logger.error("GITEA_URL not set")
|
||||||
|
if not config['token_set']:
|
||||||
|
logger.error("GITEA_TOKEN or GITHUB_TOKEN not set")
|
||||||
|
else:
|
||||||
|
if not config['token_set']:
|
||||||
|
logger.error("GITHUB_TOKEN not set")
|
@ -1,8 +1,20 @@
|
|||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
from github import Github
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the gitea-shim to the path
|
||||||
|
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
|
||||||
|
sys.path.insert(0, str(gitea_shim_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_shim import get_github_client, is_gitea_mode
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to regular GitHub if shim is not available
|
||||||
|
from github import Github as get_github_client
|
||||||
|
is_gitea_mode = lambda: False
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -14,7 +26,11 @@ def verify_pr_ownership(
|
|||||||
expected_repo,
|
expected_repo,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
gh = Github(os.environ.get("GITHUB_TOKEN"))
|
# Use the shim to get GitHub-compatible client
|
||||||
|
gh = get_github_client()
|
||||||
|
|
||||||
|
# Log which API we're using
|
||||||
|
logger.info(f"Using {'Gitea' if is_gitea_mode() else 'GitHub'} API for PR verification")
|
||||||
|
|
||||||
match = re.match(r"https://github.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url)
|
match = re.match(r"https://github.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url)
|
||||||
if not match:
|
if not match:
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
"""Task decomposition workflow implementation."""
|
"""Task decomposition workflow implementation."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from github import Github
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the gitea-shim to the path
|
||||||
|
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
|
||||||
|
sys.path.insert(0, str(gitea_shim_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_shim import get_github_client, is_gitea_mode
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to regular GitHub if shim is not available
|
||||||
|
from github import Github as get_github_client
|
||||||
|
is_gitea_mode = lambda: False
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from prometheus_swarm.workflows.base import Workflow
|
from prometheus_swarm.workflows.base import Workflow
|
||||||
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
|
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
|
||||||
@ -99,9 +112,9 @@ class RepoSummarizerWorkflow(Workflow):
|
|||||||
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
|
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
|
||||||
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
|
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
|
||||||
|
|
||||||
# Get the default branch from GitHub
|
# Get the default branch from GiteaGitHubShim
|
||||||
try:
|
try:
|
||||||
gh = Github(os.getenv("GITHUB_TOKEN"))
|
gh = GiteaGitHubShim(os.getenv("GITHUB_TOKEN"), base_url=os.getenv("GITEA_URL"))
|
||||||
self.context["repo_full_name"] = (
|
self.context["repo_full_name"] = (
|
||||||
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
f"{self.context['repo_owner']}/{self.context['repo_name']}"
|
||||||
)
|
)
|
||||||
@ -111,6 +124,7 @@ class RepoSummarizerWorkflow(Workflow):
|
|||||||
)
|
)
|
||||||
self.context["base"] = repo.default_branch
|
self.context["base"] = repo.default_branch
|
||||||
log_key_value("Default branch", self.context["base"])
|
log_key_value("Default branch", self.context["base"])
|
||||||
|
log_key_value("API Mode", "GiteaGitHubShim")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_error(e, "Failed to get default branch, using 'main'")
|
log_error(e, "Failed to get default branch, using 'main'")
|
||||||
self.context["base"] = "main"
|
self.context["base"] = "main"
|
||||||
|
@ -1,7 +1,20 @@
|
|||||||
"""Task decomposition workflow implementation."""
|
"""Task decomposition workflow implementation."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from github import Github
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the gitea-shim to the path
|
||||||
|
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
|
||||||
|
sys.path.insert(0, str(gitea_shim_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from gitea_shim import get_github_client, is_gitea_mode
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to regular GitHub if shim is not available
|
||||||
|
from github import Github as get_github_client
|
||||||
|
is_gitea_mode = lambda: False
|
||||||
|
|
||||||
from prometheus_swarm.workflows.base import Workflow
|
from prometheus_swarm.workflows.base import Workflow
|
||||||
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
|
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
|
||||||
from src.workflows.repoSummarizerAudit import phases
|
from src.workflows.repoSummarizerAudit import phases
|
||||||
@ -89,10 +102,15 @@ class repoSummarizerAuditWorkflow(Workflow):
|
|||||||
self.context["github_token"] = os.getenv("GITHUB_TOKEN")
|
self.context["github_token"] = os.getenv("GITHUB_TOKEN")
|
||||||
# Enter repo directory
|
# Enter repo directory
|
||||||
os.chdir(self.context["repo_path"])
|
os.chdir(self.context["repo_path"])
|
||||||
gh = Github(self.context["github_token"])
|
|
||||||
|
# Use the shim to get GitHub-compatible client
|
||||||
|
gh = get_github_client()
|
||||||
repo = gh.get_repo(f"{self.context['repo_owner']}/{self.context['repo_name']}")
|
repo = gh.get_repo(f"{self.context['repo_owner']}/{self.context['repo_name']}")
|
||||||
pr = repo.get_pull(self.context["pr_number"])
|
pr = repo.get_pull(self.context["pr_number"])
|
||||||
self.context["pr"] = pr
|
self.context["pr"] = pr
|
||||||
|
|
||||||
|
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
|
||||||
|
|
||||||
# Add remote for PR's repository and fetch the branch
|
# Add remote for PR's repository and fetch the branch
|
||||||
os.system(
|
os.system(
|
||||||
f"git remote add pr_source https://github.com/{pr.head.repo.full_name}"
|
f"git remote add pr_source https://github.com/{pr.head.repo.full_name}"
|
||||||
|
Reference in New Issue
Block a user