Skip to content

Commit 88eacca

Browse files
committed
Fix timeline not being properly incremental
1 parent d0c81f7 commit 88eacca

2 files changed

Lines changed: 57 additions & 22 deletions

File tree

github_backup/github_backup.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2757,19 +2757,24 @@ def backup_issues(args, repo_cwd, repository, repos_template):
27572757
stale_timelines = find_stale_timeline_issues(
27582758
args, issue_cwd, repository, include_pulls=not should_include_pulls
27592759
)
2760-
for number in sorted(stale_timelines - set(issues)):
2761-
try:
2762-
issues[number] = retrieve_data(
2763-
args, "{0}/{1}".format(_issue_template, number), paginated=False
2764-
)[0]
2765-
except (HTTPError, RepositoryUnavailableError, IndexError) as e:
2766-
# Deleted or transferred between the sweep and this fetch. The
2767-
# refresh is opportunistic, so skip rather than fail the run.
2768-
logger.warning(
2769-
"Unable to refresh cross-references for issue {0}: {1}".format(
2770-
number, e
2771-
)
2772-
)
2760+
2761+
# Issues selected only by the cross-reference sweep have not otherwise
2762+
# changed. Reuse their complete stored payload and refresh only timeline_data.
2763+
# Fetching the issue, comments, events and attachments again turns the first
2764+
# --issue-timeline run into a near-full backup and can discard optional data
2765+
# when the corresponding flags are absent.
2766+
timeline_only_issues = stale_timelines - set(issues)
2767+
for number in sorted(timeline_only_issues):
2768+
existing = read_json_file_if_exists(
2769+
"{0}/{1}.json".format(issue_cwd, number)
2770+
)
2771+
if existing is None:
2772+
logger.warning(
2773+
"Unable to refresh cross-references for issue {0}: "
2774+
"stored issue data is unreadable".format(number)
2775+
)
2776+
continue
2777+
issues[number] = existing
27732778

27742779
if issues_skipped:
27752780
issues_skipped_message = " (skipped {0} pull requests)".format(issues_skipped)
@@ -2784,6 +2789,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
27842789
timeline_template = _issue_template + "/{0}/timeline"
27852790
for number, issue in list(issues.items()):
27862791
issue_file = "{0}/{1}.json".format(issue_cwd, number)
2792+
timeline_only_refresh = number in timeline_only_issues
27872793
if (
27882794
args.incremental_by_files
27892795
and os.path.isfile(issue_file)
@@ -2799,10 +2805,14 @@ def backup_issues(args, repo_cwd, repository, repos_template):
27992805
)
28002806
continue
28012807

2802-
if args.include_issue_comments or args.include_everything:
2808+
if not timeline_only_refresh and (
2809+
args.include_issue_comments or args.include_everything
2810+
):
28032811
template = comments_template.format(number)
28042812
issues[number]["comment_data"] = retrieve_data(args, template)
2805-
if args.include_issue_events or args.include_everything:
2813+
if not timeline_only_refresh and (
2814+
args.include_issue_events or args.include_everything
2815+
):
28062816
template = events_template.format(number)
28072817
issues[number]["event_data"] = retrieve_data(args, template)
28082818
if include_timeline:
@@ -2815,7 +2825,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
28152825
for item in retrieve_data(args, template)
28162826
if item.get("event") != "commented"
28172827
]
2818-
if args.include_attachments:
2828+
if args.include_attachments and not timeline_only_refresh:
28192829
download_attachments(
28202830
args, issue_cwd, issues[number], number, repository, item_type="issue"
28212831
)

tests/test_issue_timeline.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,35 +274,60 @@ def _boom(*a, **kw):
274274
def test_stale_issue_backed_up_though_since_excludes_it(
275275
create_args, tmp_path, monkeypatch
276276
):
277-
"""The whole point: an issue since cannot see is still refreshed."""
278-
_stored(tmp_path, 7, [])
277+
"""A sweep-only issue refreshes just its timeline and preserves its data."""
278+
issue_cwd = tmp_path / "issues"
279+
issue_cwd.mkdir()
280+
stored = _issue(
281+
7,
282+
body="stored body",
283+
comment_data=[{"id": 1, "body": "stored comment"}],
284+
event_data=[{"id": 2, "event": "referenced"}],
285+
timeline_data=[],
286+
)
287+
(issue_cwd / "7.json").write_text(json.dumps(stored))
279288
args = _sweep_args(
280289
create_args,
281290
include_issues=True,
291+
include_issue_comments=True,
292+
include_issue_events=True,
282293
include_issue_timeline=True,
294+
include_attachments=True,
283295
since="2026-07-01T00:00:00Z",
284296
)
285297
timeline = [_xref("2026-07-20T00:00:00Z")]
286298
calls = []
287299
monkeypatch.setattr(
288300
github_backup,
289301
"retrieve_data",
290-
_fake_retrieve(
291-
{"/issues": [], "/issues/7": [_issue(7)], "/7/timeline": timeline}, calls
292-
),
302+
_fake_retrieve({"/issues": [], "/7/timeline": timeline}, calls),
293303
)
294304
monkeypatch.setattr(
295305
github_backup,
296306
"retrieve_graphql_data",
297307
_graphql_state({7: (1, "2026-07-20T00:00:00Z")}),
298308
)
309+
attachment_calls = []
310+
monkeypatch.setattr(
311+
github_backup,
312+
"download_attachments",
313+
lambda *args, **kwargs: attachment_calls.append((args, kwargs)),
314+
)
299315

300316
github_backup.backup_issues(
301317
args, str(tmp_path), {"full_name": "owner/repo"}, "https://api.github.com/repos"
302318
)
303319

304-
saved = json.loads((tmp_path / "issues" / "7.json").read_text())
320+
saved = json.loads((issue_cwd / "7.json").read_text())
305321
assert saved["timeline_data"] == timeline
322+
assert saved["body"] == "stored body"
323+
assert saved["comment_data"] == stored["comment_data"]
324+
assert saved["event_data"] == stored["event_data"]
325+
assert calls == [
326+
"https://api.github.com/repos/owner/repo/issues",
327+
"https://api.github.com/repos/owner/repo/issues",
328+
"https://api.github.com/repos/owner/repo/issues/7/timeline",
329+
]
330+
assert attachment_calls == []
306331

307332

308333
def test_sweep_follows_pagination_cursors(create_args, tmp_path, monkeypatch):

0 commit comments

Comments
 (0)