mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 11:36:44 +03:00
Updated PR as issues
This commit is contained in:
27
README.md
27
README.md
@@ -36,7 +36,7 @@ First user signup becomes admin. Configure GitHub and Gitea through the web inte
|
|||||||
- 🏢 Mirror entire organizations with flexible strategies
|
- 🏢 Mirror entire organizations with flexible strategies
|
||||||
- 🎯 Custom destination control for repos and organizations
|
- 🎯 Custom destination control for repos and organizations
|
||||||
- 📦 **Git LFS support** - Mirror large files with Git LFS
|
- 📦 **Git LFS support** - Mirror large files with Git LFS
|
||||||
- 📝 **Metadata mirroring** - Issues, PRs, labels, milestones, wiki
|
- 📝 **Metadata mirroring** - Issues, pull requests (as issues), labels, milestones, wiki
|
||||||
- 🚫 **Repository ignore** - Mark specific repos to skip
|
- 🚫 **Repository ignore** - Mark specific repos to skip
|
||||||
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC)
|
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC)
|
||||||
- 📊 Real-time dashboard with activity logs
|
- 📊 Real-time dashboard with activity logs
|
||||||
@@ -311,6 +311,31 @@ Gitea Mirror can also act as an OIDC provider for other applications. Register O
|
|||||||
- Create service-to-service authentication
|
- Create service-to-service authentication
|
||||||
- Build integrations with your Gitea Mirror instance
|
- Build integrations with your Gitea Mirror instance
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### Pull Request Mirroring Implementation
|
||||||
|
Pull requests **cannot be created as actual PRs** in Gitea due to API limitations. Instead, they are mirrored as **enriched issues** with comprehensive metadata.
|
||||||
|
|
||||||
|
**Why real PR mirroring isn't possible:**
|
||||||
|
- Gitea's API doesn't support creating pull requests from external sources
|
||||||
|
- Real PRs require actual Git branches with commits to exist in the repository
|
||||||
|
- Would require complex branch synchronization and commit replication
|
||||||
|
- The mirror relationship is one-way (GitHub → Gitea) for repository content
|
||||||
|
|
||||||
|
**How we handle Pull Requests:**
|
||||||
|
PRs are mirrored as issues with rich metadata including:
|
||||||
|
- 🏷️ Special "pull-request" label for identification
|
||||||
|
- 📌 [PR #number] prefix in title with status indicators ([MERGED], [CLOSED])
|
||||||
|
- 👤 Original author and creation date
|
||||||
|
- 📝 Complete commit history (up to 10 commits with links)
|
||||||
|
- 📊 File changes summary with additions/deletions
|
||||||
|
- 📁 List of modified files (up to 20 files)
|
||||||
|
- 💬 Original PR description and comments
|
||||||
|
- 🔀 Base and head branch information
|
||||||
|
- ✅ Merge status tracking
|
||||||
|
|
||||||
|
This approach preserves all important PR information while working within Gitea's API constraints. The PRs appear in Gitea's issue tracker with clear visual distinction and comprehensive details.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Checkbox } from "../ui/checkbox";
|
import { Checkbox } from "../ui/checkbox";
|
||||||
import type { MirrorOptions } from "@/types/config";
|
import type { MirrorOptions } from "@/types/config";
|
||||||
import { RefreshCw, Info } from "lucide-react";
|
import { RefreshCw, Info } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "../ui/tooltip";
|
||||||
|
|
||||||
interface MirrorOptionsFormProps {
|
interface MirrorOptionsFormProps {
|
||||||
config: MirrorOptions;
|
config: MirrorOptions;
|
||||||
@@ -27,7 +32,7 @@ export function MirrorOptionsForm({
|
|||||||
if (!checked) {
|
if (!checked) {
|
||||||
newConfig.metadataComponents = {
|
newConfig.metadataComponents = {
|
||||||
issues: false,
|
issues: false,
|
||||||
pullRequests: false,
|
pullRequests: false, // Keep for backwards compatibility but not shown in UI
|
||||||
labels: false,
|
labels: false,
|
||||||
milestones: false,
|
milestones: false,
|
||||||
wiki: false,
|
wiki: false,
|
||||||
@@ -188,8 +193,33 @@ export function MirrorOptionsForm({
|
|||||||
htmlFor="metadata-pullRequests"
|
htmlFor="metadata-pullRequests"
|
||||||
className="ml-2 text-sm select-none"
|
className="ml-2 text-sm select-none"
|
||||||
>
|
>
|
||||||
Pull requests
|
Pull Requests (as issues)
|
||||||
</label>
|
</label>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3 w-3 ml-1 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className="max-w-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-semibold">Pull Requests are mirrored as issues</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
Due to Gitea API limitations, PRs cannot be created as actual pull requests.
|
||||||
|
Instead, they are mirrored as issues with:
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs space-y-1 ml-3">
|
||||||
|
<li>• [PR #number] prefix in title</li>
|
||||||
|
<li>• Full PR description and metadata</li>
|
||||||
|
<li>• Commit history (up to 10 commits)</li>
|
||||||
|
<li>• File changes summary</li>
|
||||||
|
<li>• Diff preview (first 5 files)</li>
|
||||||
|
<li>• Review comments preserved</li>
|
||||||
|
<li>• Merge/close status tracking</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
121
src/lib/gitea.ts
121
src/lib/gitea.ts
@@ -1593,17 +1593,90 @@ export async function mirrorGitRepoPullRequestsToGitea({
|
|||||||
|
|
||||||
const { processWithRetry } = await import("@/lib/utils/concurrency");
|
const { processWithRetry } = await import("@/lib/utils/concurrency");
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
await processWithRetry(
|
await processWithRetry(
|
||||||
pullRequests,
|
pullRequests,
|
||||||
async (pr) => {
|
async (pr) => {
|
||||||
const issueData = {
|
|
||||||
title: `[PR #${pr.number}] ${pr.title}`,
|
|
||||||
body: `**Original Pull Request:** ${pr.html_url}\n\n**State:** ${pr.state}\n**Merged:** ${pr.merged_at ? 'Yes' : 'No'}\n\n---\n\n${pr.body || 'No description provided'}`,
|
|
||||||
labels: [{ name: "pull-request" }],
|
|
||||||
state: pr.state === "closed" ? "closed" : "open",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch additional PR data for rich metadata
|
||||||
|
const [prDetail, commits, files] = await Promise.all([
|
||||||
|
octokit.rest.pulls.get({ owner, repo, pull_number: pr.number }),
|
||||||
|
octokit.rest.pulls.listCommits({ owner, repo, pull_number: pr.number, per_page: 10 }),
|
||||||
|
octokit.rest.pulls.listFiles({ owner, repo, pull_number: pr.number, per_page: 100 })
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build rich PR body with metadata
|
||||||
|
let richBody = `## 📋 Pull Request Information\n\n`;
|
||||||
|
richBody += `**Original PR:** ${pr.html_url}\n`;
|
||||||
|
richBody += `**Author:** [@${pr.user?.login}](${pr.user?.html_url})\n`;
|
||||||
|
richBody += `**Created:** ${new Date(pr.created_at).toLocaleDateString()}\n`;
|
||||||
|
richBody += `**Status:** ${pr.state === 'closed' ? (pr.merged_at ? '✅ Merged' : '❌ Closed') : '🔄 Open'}\n`;
|
||||||
|
|
||||||
|
if (pr.merged_at) {
|
||||||
|
richBody += `**Merged:** ${new Date(pr.merged_at).toLocaleDateString()}\n`;
|
||||||
|
richBody += `**Merged by:** [@${prDetail.data.merged_by?.login}](${prDetail.data.merged_by?.html_url})\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
richBody += `\n**Base:** \`${pr.base.ref}\` ← **Head:** \`${pr.head.ref}\`\n`;
|
||||||
|
richBody += `\n---\n\n`;
|
||||||
|
|
||||||
|
// Add commit history (up to 10 commits)
|
||||||
|
if (commits.data.length > 0) {
|
||||||
|
richBody += `### 📝 Commits (${commits.data.length}${commits.data.length >= 10 ? '+' : ''})\n\n`;
|
||||||
|
commits.data.slice(0, 10).forEach(commit => {
|
||||||
|
const shortSha = commit.sha.substring(0, 7);
|
||||||
|
richBody += `- [\`${shortSha}\`](${commit.html_url}) ${commit.commit.message.split('\n')[0]}\n`;
|
||||||
|
});
|
||||||
|
if (commits.data.length > 10) {
|
||||||
|
richBody += `\n_...and ${commits.data.length - 10} more commits_\n`;
|
||||||
|
}
|
||||||
|
richBody += `\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file changes summary
|
||||||
|
if (files.data.length > 0) {
|
||||||
|
const additions = prDetail.data.additions || 0;
|
||||||
|
const deletions = prDetail.data.deletions || 0;
|
||||||
|
const changedFiles = prDetail.data.changed_files || files.data.length;
|
||||||
|
|
||||||
|
richBody += `### 📊 Changes\n\n`;
|
||||||
|
richBody += `**${changedFiles} file${changedFiles !== 1 ? 's' : ''} changed** `;
|
||||||
|
richBody += `(+${additions} additions, -${deletions} deletions)\n\n`;
|
||||||
|
|
||||||
|
// List changed files (up to 20)
|
||||||
|
richBody += `<details>\n<summary>View changed files</summary>\n\n`;
|
||||||
|
files.data.slice(0, 20).forEach(file => {
|
||||||
|
const changeIndicator = file.status === 'added' ? '➕' :
|
||||||
|
file.status === 'removed' ? '➖' : '📝';
|
||||||
|
richBody += `${changeIndicator} \`${file.filename}\` (+${file.additions} -${file.deletions})\n`;
|
||||||
|
});
|
||||||
|
if (files.data.length > 20) {
|
||||||
|
richBody += `\n_...and ${files.data.length - 20} more files_\n`;
|
||||||
|
}
|
||||||
|
richBody += `\n</details>\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add original PR description
|
||||||
|
richBody += `### 📄 Description\n\n`;
|
||||||
|
richBody += pr.body || '_No description provided_';
|
||||||
|
richBody += `\n\n---\n`;
|
||||||
|
richBody += `\n<sub>🔄 This issue represents a GitHub Pull Request. `;
|
||||||
|
richBody += `It cannot be merged through Gitea due to API limitations.</sub>`;
|
||||||
|
|
||||||
|
// Prepare issue title with status indicator
|
||||||
|
const statusPrefix = pr.merged_at ? '[MERGED] ' : (pr.state === 'closed' ? '[CLOSED] ' : '');
|
||||||
|
const issueTitle = `[PR #${pr.number}] ${statusPrefix}${pr.title}`;
|
||||||
|
|
||||||
|
const issueData = {
|
||||||
|
title: issueTitle,
|
||||||
|
body: richBody,
|
||||||
|
labels: [{ name: "pull-request" }],
|
||||||
|
state: pr.state === "closed" ? "closed" : "open",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[Pull Requests] Creating enriched issue for PR #${pr.number}: ${pr.title}`);
|
||||||
await httpPost(
|
await httpPost(
|
||||||
`${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repository.name}/issues`,
|
`${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repository.name}/issues`,
|
||||||
issueData,
|
issueData,
|
||||||
@@ -1611,10 +1684,34 @@ export async function mirrorGitRepoPullRequestsToGitea({
|
|||||||
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
successCount++;
|
||||||
console.error(
|
console.log(`[Pull Requests] ✅ Successfully created issue for PR #${pr.number}`);
|
||||||
`Failed to mirror PR #${pr.number}: ${error instanceof Error ? error.message : String(error)}`
|
} catch (apiError) {
|
||||||
);
|
// If the detailed fetch fails, fall back to basic PR info
|
||||||
|
console.log(`[Pull Requests] Falling back to basic info for PR #${pr.number} due to error: ${apiError}`);
|
||||||
|
const basicIssueData = {
|
||||||
|
title: `[PR #${pr.number}] ${pr.title}`,
|
||||||
|
body: `**Original Pull Request:** ${pr.html_url}\n\n**State:** ${pr.state}\n**Merged:** ${pr.merged_at ? 'Yes' : 'No'}\n\n---\n\n${pr.body || 'No description provided'}`,
|
||||||
|
labels: [{ name: "pull-request" }],
|
||||||
|
state: pr.state === "closed" ? "closed" : "open",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await httpPost(
|
||||||
|
`${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repository.name}/issues`,
|
||||||
|
basicIssueData,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
successCount++;
|
||||||
|
console.log(`[Pull Requests] ✅ Created basic issue for PR #${pr.number}`);
|
||||||
|
} catch (error) {
|
||||||
|
failedCount++;
|
||||||
|
console.error(
|
||||||
|
`[Pull Requests] ❌ Failed to mirror PR #${pr.number}: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1624,7 +1721,7 @@ export async function mirrorGitRepoPullRequestsToGitea({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ Mirrored ${pullRequests.length} pull requests to Gitea`);
|
console.log(`✅ Mirrored ${successCount}/${pullRequests.length} pull requests to Gitea as enriched issues (${failedCount} failed)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mirrorGitRepoLabelsToGitea({
|
export async function mirrorGitRepoLabelsToGitea({
|
||||||
|
|||||||
Reference in New Issue
Block a user