Could Gitea-Mirror use GitHub’s repository-wide incremental endpoints for scheduled metadata syncs instead of checking comments on every historical issue/PR?
GET /repos/{owner}/{repo}/issues?state=all&since=<last_sync>&sort=updated
GET /repos/{owner}/{repo}/issues/comments?since=<last_sync>&sort=updated
GET /repos/{owner}/{repo}/pulls/comments?since=<last_sync>&sort=updated
These endpoints allow only changed/added issues, PRs, and comments since the previous successful sync to be fetched.
For large repositories, the current per-issue comment scanning causes thousands of API calls and can exhaust GitHub’s 5,000 requests/hour limit before the sync completes.
An incremental approach should reduce API usage dramatically and make recurring metadata syncs practical.
Could Gitea-Mirror use GitHub’s repository-wide incremental endpoints for scheduled metadata syncs instead of checking comments on every historical issue/PR?
```http
GET /repos/{owner}/{repo}/issues?state=all&since=<last_sync>&sort=updated
GET /repos/{owner}/{repo}/issues/comments?since=<last_sync>&sort=updated
GET /repos/{owner}/{repo}/pulls/comments?since=<last_sync>&sort=updated
```
These endpoints allow only changed/added issues, PRs, and comments since the previous successful sync to be fetched.
For large repositories, the current per-issue comment scanning causes thousands of API calls and can exhaust GitHub’s 5,000 requests/hour limit before the sync completes.
An incremental approach should reduce API usage dramatically and make recurring metadata syncs practical.
<!-- github-sync-id:5554868325 -->
Thanks, this is accurate and worth doing. Today every metadata sync lists all issues and all pull requests and then makes one comment call per issue and four calls per pull request, so a large repository does go past the hourly limit. The client already sends ETags, and a 304 does not count against the limit, but that cache lives in memory, is capped, and every 304 is still a round trip, so it only softens the problem.
The plan: keep the first sync full, then on later syncs list issues with since set to the last completed pass (with a safety margin) and only reconcile the issues and pull requests that changed, which is also the only set whose comments and details get fetched. A periodic full pass and the existing reset metadata action stay as the fallback for anything since cannot see. Work is in progress; it will land in the next minor release.
Thanks, this is accurate and worth doing. Today every metadata sync lists all issues and all pull requests and then makes one comment call per issue and four calls per pull request, so a large repository does go past the hourly limit. The client already sends ETags, and a 304 does not count against the limit, but that cache lives in memory, is capped, and every 304 is still a round trip, so it only softens the problem.
The plan: keep the first sync full, then on later syncs list issues with `since` set to the last completed pass (with a safety margin) and only reconcile the issues and pull requests that changed, which is also the only set whose comments and details get fetched. A periodic full pass and the existing reset metadata action stay as the fallback for anything `since` cannot see. Work is in progress; it will land in the next minor release.
<!-- github-comment-id:5799462846 -->
After the first complete pass, the issues and pull request passes now ask GitHub only for items updated since the previous run (minus a 10 minute margin), and fetch comments, PR details, commits and files only for those. Pull requests go through the issues endpoint for this since pulls.list has no since filter.
A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs. The watermark only advances when a pass finishes with no failed item, so nothing gets skipped because of a transient error.
No action needed on existing installs. If you see the request count not dropping after the first sync, the [Issues] and [Pull Requests] log lines say which mode each pass ran in and why.
Done in #450 and released in v3.39.0.
After the first complete pass, the issues and pull request passes now ask GitHub only for items updated since the previous run (minus a 10 minute margin), and fetch comments, PR details, commits and files only for those. Pull requests go through the issues endpoint for this since `pulls.list` has no `since` filter.
A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs. The watermark only advances when a pass finishes with no failed item, so nothing gets skipped because of a transient error.
No action needed on existing installs. If you see the request count not dropping after the first sync, the `[Issues]` and `[Pull Requests]` log lines say which mode each pass ran in and why.
<!-- github-comment-id:5799798875 -->
A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs.
Does this mean that in all these cases, always a full pass will be done? If yes, then this is still an issue for me. With many large repos, it will never get to the state of a full pass for all repos within that 7 days, and then it will already start again for a new full pass.
And with upgrading, you mean upgrade of gitea-mirror?
And I get these errors:
[Batch] Stopping before item 165 of 245: source is rate limited
[Sync] Error while syncing repository <REDACTED>: 126 | }
127 | }
128 | throw lastError || /* @__PURE__ */ new Error("Unknown error occurred");
129 | };
130 | const results = await processInParallel(items, processWithRetryLogic, concurrencyLimit, onProgress, () => rateLimitError !== null);
131 | if (rateLimitError) throw new RateLimitedBatchError(rateLimitError, results, items.length - results.length);
^
RateLimitedBatchError: Rate limit hit while processing items; stopped with 84 item(s) left (API rate limit exceeded for user ID <REDACTED>. If you reach out to GitHub Support for help, please include the request ID <REDACTED> and timestamp 2026-09-24 15:06:32 UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting)
status: 403,
results: [
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined,
... 61 more items
],
at processWithRetry (/app/dist/server/chunks/concurrency_CO4qU4tK.mjs:131:28)
119 | request: requestOptions
120 | });
121 | }
122 | if (status >= 400) {
123 | octokitResponse.data = await getResponseData(fetchResponse);
124 | throw new RequestError(toErrorMessage(octokitResponse.data), status, {
^
HttpError: API rate limit exceeded for user ID <REDACTED>. If you reach out to GitHub Support for help, please include the request ID <REDACTED> and timestamp 2026-09-24 15:06:32 UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting
status: 403,
request: {
method: "GET",
url: "https://api.github.com/repos/<REDACTED>/pulls/218",
headers: [Object ...],
request: [Object ...],
},
response: {
url: "https://api.github.com/repos/<REDACTED>/pulls/218",
status: 403,
headers: [Object ...],
data: [Object ...],
},
at fetchWrapper (/app/node_modules/@octokit/request/dist-bundle/index.js:124:15)
at processTicksAndRejections (unknown:7:39)
`A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs.`
Does this mean that in all these cases, always a full pass will be done? If yes, then this is still an issue for me. With many large repos, it will never get to the state of a full pass for all repos within that 7 days, and then it will already start again for a new full pass.
And with `upgrading`, you mean upgrade of gitea-mirror?
And I get these errors:
```
[Batch] Stopping before item 165 of 245: source is rate limited
[Sync] Error while syncing repository <REDACTED>: 126 | }
127 | }
128 | throw lastError || /* @__PURE__ */ new Error("Unknown error occurred");
129 | };
130 | const results = await processInParallel(items, processWithRetryLogic, concurrencyLimit, onProgress, () => rateLimitError !== null);
131 | if (rateLimitError) throw new RateLimitedBatchError(rateLimitError, results, items.length - results.length);
^
RateLimitedBatchError: Rate limit hit while processing items; stopped with 84 item(s) left (API rate limit exceeded for user ID <REDACTED>. If you reach out to GitHub Support for help, please include the request ID <REDACTED> and timestamp 2026-09-24 15:06:32 UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting)
status: 403,
results: [
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, undefined,
... 61 more items
],
at processWithRetry (/app/dist/server/chunks/concurrency_CO4qU4tK.mjs:131:28)
119 | request: requestOptions
120 | });
121 | }
122 | if (status >= 400) {
123 | octokitResponse.data = await getResponseData(fetchResponse);
124 | throw new RequestError(toErrorMessage(octokitResponse.data), status, {
^
HttpError: API rate limit exceeded for user ID <REDACTED>. If you reach out to GitHub Support for help, please include the request ID <REDACTED> and timestamp 2026-09-24 15:06:32 UTC. For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) - https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#rate-limiting
status: 403,
request: {
method: "GET",
url: "https://api.github.com/repos/<REDACTED>/pulls/218",
headers: [Object ...],
request: [Object ...],
},
response: {
url: "https://api.github.com/repos/<REDACTED>/pulls/218",
status: 403,
headers: [Object ...],
data: [Object ...],
},
at fetchWrapper (/app/node_modules/@octokit/request/dist-bundle/index.js:124:15)
at processTicksAndRejections (unknown:7:39)
```
<!-- github-comment-id:5816986635 -->
Yes, "upgrading" means upgrading gitea-mirror. A repository synced by an older version has no watermark yet, so its first sync on 3.39.0 or later is a full pass.
You are right that in 3.39.0 a full pass was expensive every time. It fetched comments for every issue, and details, commits and files for every pull request. A pass stopped by the rate limit also started over on the next run, so on large repositories it might never finish. #453 changes both:
A pass stopped by the rate limit saves the items it finished. The next run continues from there and only redoes an item if it changed in the meantime.
The weekly full pass still lists everything, but it only fetches details for items that are missing in Gitea or changed since the last completed pass. For a large repository that is about one request per hundred items instead of several per item.
The log you posted is the new rate limit handling from 3.40.1 doing its job. It stopped at item 165 instead of sending the remaining 84 requests. The error dump was too noisy though. It is now a single line, and the activity entry says the sync was paused instead of failed.
This will be in the next release, and I will post here when it is out.
Thanks, both points are fair.
Yes, "upgrading" means upgrading gitea-mirror. A repository synced by an older version has no watermark yet, so its first sync on 3.39.0 or later is a full pass.
You are right that in 3.39.0 a full pass was expensive every time. It fetched comments for every issue, and details, commits and files for every pull request. A pass stopped by the rate limit also started over on the next run, so on large repositories it might never finish. #453 changes both:
- A pass stopped by the rate limit saves the items it finished. The next run continues from there and only redoes an item if it changed in the meantime.
- The weekly full pass still lists everything, but it only fetches details for items that are missing in Gitea or changed since the last completed pass. For a large repository that is about one request per hundred items instead of several per item.
The log you posted is the new rate limit handling from 3.40.1 doing its job. It stopped at item 165 instead of sending the remaining 84 requests. The error dump was too noisy though. It is now a single line, and the activity entry says the sync was paused instead of failed.
This will be in the next release, and I will post here when it is out.
<!-- github-comment-id:5825738226 -->
After upgrading, a pass that stops at the rate limit continues where it left off on the next run. The weekly full pass also only fetches details for items that are missing or changed. The log should show lines like Resuming an unfinished full pass and unchanged since the last pass. If a pass still never completes on your larger repositories, please post the [Issues] and [Pull Requests] lines from a couple of runs here.
@hunter-nl this is out in v3.40.2: https://github.com/RayLabsHQ/gitea-mirror/releases/tag/v3.40.2
After upgrading, a pass that stops at the rate limit continues where it left off on the next run. The weekly full pass also only fetches details for items that are missing or changed. The log should show lines like `Resuming an unfinished full pass` and `unchanged since the last pass`. If a pass still never completes on your larger repositories, please post the `[Issues]` and `[Pull Requests]` lines from a couple of runs here.
<!-- github-comment-id:5825813858 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Could Gitea-Mirror use GitHub’s repository-wide incremental endpoints for scheduled metadata syncs instead of checking comments on every historical issue/PR?
These endpoints allow only changed/added issues, PRs, and comments since the previous successful sync to be fetched.
For large repositories, the current per-issue comment scanning causes thousands of API calls and can exhaust GitHub’s 5,000 requests/hour limit before the sync completes.
An incremental approach should reduce API usage dramatically and make recurring metadata syncs practical.
Thanks, this is accurate and worth doing. Today every metadata sync lists all issues and all pull requests and then makes one comment call per issue and four calls per pull request, so a large repository does go past the hourly limit. The client already sends ETags, and a 304 does not count against the limit, but that cache lives in memory, is capped, and every 304 is still a round trip, so it only softens the problem.
The plan: keep the first sync full, then on later syncs list issues with
sinceset to the last completed pass (with a safety margin) and only reconcile the issues and pull requests that changed, which is also the only set whose comments and details get fetched. A periodic full pass and the existing reset metadata action stay as the fallback for anythingsincecannot see. Work is in progress; it will land in the next minor release.Done in #450 and released in v3.39.0.
After the first complete pass, the issues and pull request passes now ask GitHub only for items updated since the previous run (minus a 10 minute margin), and fetch comments, PR details, commits and files only for those. Pull requests go through the issues endpoint for this since
pulls.listhas nosincefilter.A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs. The watermark only advances when a pass finishes with no failed item, so nothing gets skipped because of a transient error.
No action needed on existing installs. If you see the request count not dropping after the first sync, the
[Issues]and[Pull Requests]log lines say which mode each pass ran in and why.A full pass still runs on the first sync after upgrading, after Reset metadata, when the last full pass is 7 or more days old, and when the destination has no mirrored issues or PRs.Does this mean that in all these cases, always a full pass will be done? If yes, then this is still an issue for me. With many large repos, it will never get to the state of a full pass for all repos within that 7 days, and then it will already start again for a new full pass.
And with
upgrading, you mean upgrade of gitea-mirror?And I get these errors:
Thanks, both points are fair.
Yes, "upgrading" means upgrading gitea-mirror. A repository synced by an older version has no watermark yet, so its first sync on 3.39.0 or later is a full pass.
You are right that in 3.39.0 a full pass was expensive every time. It fetched comments for every issue, and details, commits and files for every pull request. A pass stopped by the rate limit also started over on the next run, so on large repositories it might never finish. #453 changes both:
The log you posted is the new rate limit handling from 3.40.1 doing its job. It stopped at item 165 instead of sending the remaining 84 requests. The error dump was too noisy though. It is now a single line, and the activity entry says the sync was paused instead of failed.
This will be in the next release, and I will post here when it is out.
@hunter-nl this is out in v3.40.2: https://github.com/RayLabsHQ/gitea-mirror/releases/tag/v3.40.2
After upgrading, a pass that stops at the rate limit continues where it left off on the next run. The weekly full pass also only fetches details for items that are missing or changed. The log should show lines like
Resuming an unfinished full passandunchanged since the last pass. If a pass still never completes on your larger repositories, please post the[Issues]and[Pull Requests]lines from a couple of runs here.