going to test deep reply branch merge.
This commit is contained in:
1 parent
dc635997a9
commit
8741077173
40 files changed
+266
-72
No files matched your search
@@ -131,3 +131,7 @@ Quartz remotes live in `site/` (`cd site; git pull`). After a Quartz pull, re-co
|
||||
Current major is **Quartz 5**. Edit `publish/quartz.config.yaml`, not the clone. Canvas uses `@quartz-community/canvas-page`. Drafts use `@quartz-community/remove-draft`.
|
||||
|
||||
Engine license: MIT (Quartz). Vault content: Unlicense (`LICENSE`).
|
||||
|
||||
## Notice
|
||||
|
||||
I'm not a web dev. Look away at the scripts this is just done because it doesn't exist, so it was done badly.
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "0x44"
|
||||
handle: 0x44_
|
||||
post_id: "1951048949667619280"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Matthew Rusch"
|
||||
handle: 2Sexy4MyGPU
|
||||
post_id: "1950935584714654191"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Nicholas Wilt"
|
||||
handle: CUDAHandbook
|
||||
post_id: "1951018231856312578"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Nick Tasios"
|
||||
handle: Karyuutensei
|
||||
post_id: "1950905493053923408"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "vulkan tamer"
|
||||
handle: NickDrisc0ll
|
||||
post_id: "1950899486059851961"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "PunteroCrudo"
|
||||
handle: PunteroCrudo
|
||||
post_id: "1950929528131326083"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Robert William Cummings"
|
||||
handle: SquaredApe
|
||||
post_id: "1950915633811534012"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Sterence"
|
||||
handle: Stererence
|
||||
post_id: "1950970860694913461"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "VicThor Dev"
|
||||
handle: VicThorDev6
|
||||
post_id: "1950927812401004774"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Naf 👨🏽💻"
|
||||
handle: _naf_90
|
||||
post_id: "1950948334002159684"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "David Sena"
|
||||
handle: biovf
|
||||
post_id: "1950863824522739861"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "D J"
|
||||
handle: cyberbemon
|
||||
post_id: "1950940978229236154"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Ian Hanschen"
|
||||
handle: furan
|
||||
post_id: "1950955895351083303"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Ian Hanschen"
|
||||
handle: furan
|
||||
post_id: "1950956007737463015"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "NEOAethyr"
|
||||
handle: konigssohne
|
||||
post_id: "1950961762419044639"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Arnaud Carré"
|
||||
handle: leonard_coder
|
||||
post_id: "1950889332375470135"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Manuel Montoto"
|
||||
handle: m6502
|
||||
post_id: "1950913023851278616"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "𝖒𝖒𝖆𝖑𝖊𝖝"
|
||||
handle: mmalex
|
||||
post_id: "1950886322341630045"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Chrinovic Mukanya"
|
||||
handle: mu_chrinovic
|
||||
post_id: "1950864277624979480"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Michael Spector"
|
||||
handle: nullspector
|
||||
post_id: "1950865695140364456"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Boga"
|
||||
handle: o__boga
|
||||
post_id: "1950939824183001106"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "opdroid1234"
|
||||
handle: opdroid1234
|
||||
post_id: "1950902422362046528"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "PlasmaBitcoin"
|
||||
handle: plasmabitcoin
|
||||
post_id: "1950963702569771336"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "unevenprankster"
|
||||
handle: pranksterware
|
||||
post_id: "1950872517700206902"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "᠌᠌ ᠌ ᠌"
|
||||
handle: roasted6read
|
||||
post_id: "1950914379412566114"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Rúben Cabaço"
|
||||
handle: ruben_cabaco
|
||||
post_id: "1950894646881611793"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Simplex"
|
||||
handle: simplex_fx
|
||||
post_id: "1951011391827010009"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Vlad Erium 🇯🇵"
|
||||
handle: ssh4net
|
||||
post_id: "1950870099809681578"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Mark Coleman"
|
||||
handle: thecolemanthe
|
||||
post_id: "1950956186783895852"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+3
-2
@@ -7,8 +7,8 @@ author: "Alonne"
|
||||
handle: totalwert
|
||||
post_id: "1950900720770625916"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
@@ -31,6 +31,7 @@ parent_post_id: "1950860870818439202"
|
||||
@NOTimothyLottes
|
||||
|
||||
<video controls src="https://video.twimg.com/tweet_video/GxL93LLW8AA-5hM.mp4"></video>
|
||||
|
||||
## Related
|
||||
|
||||
- Spine: [[archive/threads/NOTimothyLottes/2025-07-31-people-claim-assembly-is-hard-a-good-counter-to]]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "@NOTimothyLottes I'm gonna start pretending I understood everything you said"
|
||||
type: archive
|
||||
source: twitter
|
||||
source_url: "https://x.com/AnshumanKhanna5/status/1951095609567617288"
|
||||
author: "Anshuman Khanna"
|
||||
handle: AnshumanKhanna5
|
||||
post_id: "1951095609567617288"
|
||||
date: 2025-08-01
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
- NOTimothyLottes
|
||||
description: "@NOTimothyLottes I'm gonna start pretending I understood everything you said"
|
||||
in_reply_to: ""
|
||||
parent_post_id: "1950868863836410096"
|
||||
---
|
||||
|
||||
## Source
|
||||
|
||||
- URL: https://x.com/AnshumanKhanna5/status/1951095609567617288
|
||||
- Author: Anshuman Khanna (@AnshumanKhanna5)
|
||||
- Posted: 2025-08-01 01:40:17
|
||||
|
||||
## Branch
|
||||
|
||||
**1/**
|
||||
|
||||
@NOTimothyLottes I'm gonna start pretending I understood everything you said
|
||||
|
||||
## Related
|
||||
|
||||
- Spine: [[archive/threads/NOTimothyLottes/2025-07-31-people-claim-assembly-is-hard-a-good-counter-to]]
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Anshuman Khanna"
|
||||
handle: AnshumanKhanna5
|
||||
post_id: "1951095292377571395"
|
||||
date: 2025-08-01
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Ægis Stranding"
|
||||
handle: John31625320803
|
||||
post_id: "1951198394329829393"
|
||||
date: 2025-08-01
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Karthik Kumar Viswanathan"
|
||||
handle: _vkaku
|
||||
post_id: "1951170620542296537"
|
||||
date: 2025-08-01
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ author: "Ben Schulz"
|
||||
handle: schulzb589
|
||||
post_id: "1951093394786361833"
|
||||
date: 2025-08-01
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
|
||||
+4
-2
@@ -7,8 +7,8 @@ author: "NOTimothyLottes"
|
||||
handle: NOTimothyLottes
|
||||
post_id: "1950860870818439202"
|
||||
date: 2025-07-31
|
||||
archived: 2026-08-23
|
||||
draft: true
|
||||
archived: 2026-08-24
|
||||
draft: false
|
||||
tags:
|
||||
- archive
|
||||
- twitter
|
||||
@@ -50,3 +50,5 @@ So Valve standardizing on Proton saves a lot of trouble with porting across ABIs
|
||||
**6/**
|
||||
|
||||
Few probably remember the old Unix before pthreads and ELF. It was better then. Much easier to use fork and shm to do platform audio than today. And that holds for all the vintage workstations (which I did implement back in the day)
|
||||
|
||||
Branches: [[archive/threads/NOTimothyLottes/2025-07-31-people-claim-assembly-is-hard-a-good-counter-to/2025-08-01-AnshumanKhanna5-im-gonna-start-pretending-i-understood-everything]]
|
||||
@@ -9,6 +9,7 @@ python C:\projects\Threadwell\scripts\twitter\tw.py graph --id <snowflake>
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py refresh --id <snowflake> --tip
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py refresh --id <spine-tip> --tip --branch <branch-tip>
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py add-branch --id <original-spine-tip> --from <reply-node>
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py emit --id <spine-tip> --tip --preserve-existing
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py audit-media --id <snowflake>
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py publish --id <snowflake>
|
||||
python C:\projects\Threadwell\scripts\twitter\tw.py sync --handle <handle>
|
||||
@@ -23,7 +24,9 @@ python C:\projects\Threadwell\scripts\twitter\tw.py migrate-media --all --apply
|
||||
|
||||
`refresh` = refetch (cookies) + ingest + emit `--force` + media_merge. `--tip` treats `--id` as the tip and walks back to the OP regardless of handle. Repeatable `--branch <tip-id>` captures only those additional explicit tip paths and merges them into the same `thread_data.json`; it does not discover replies automatically. Captures run sequentially with gallery-dl retries disabled and five-second extractor/request pacing. Each thread emits exactly one archive directory owned by the OP (the post with `reply_to_id == None`); cross-author responders do not get a directory. Does not flip `draft`. Does not commit. Frozen ids abort.
|
||||
|
||||
`add-branch` incrementally extends an already emitted thread. `--id` must be the original stored spine tip; repeatable `--from <reply-node>` values are each captured once. The command locally retains each node's missing attachment path and entire visible descendant subtree, merges only previously absent posts into the existing `thread_data.json`, downloads media for the new posts, and re-emits with the original spine unchanged. It preserves existing origin/local/fallback/derived/OCR media records and adds provider-origin plus local-copy records for new branch media. A visible leaf is only the deepest reply returned by that capture; the command does not query descendants individually or claim provider-wide completeness. Fallback upload remains a separate explicit action after origin unavailability is confirmed.
|
||||
`emit --preserve-existing` is the safe completion step after inspecting a fresh scratch capture. Fresh records win for ids the provider returned; posts present only in the existing emitted `thread_data.json` are retained after the command verifies both captures share a conversation root. This prevents an incomplete gallery-dl refresh from deleting archived replies. The merged scratch dataset keeps the requested `--id` as its stored spine tip before normal emission and media-manifest reconciliation.
|
||||
|
||||
`add-branch` incrementally extends an already emitted thread. `--id` must be the original stored spine tip; repeatable `--from <reply-node>` values are each captured once. The command locally retains each node's missing attachment path and entire visible descendant subtree, merges only previously absent posts into the existing `thread_data.json`, downloads media for the new posts, and re-emits with the original spine unchanged. It preserves existing origin/local/fallback/derived/OCR media records and adds provider-origin plus local-copy records for new branch media. A visible leaf is only the deepest reply returned by that capture; the command does not query descendants individually or claim provider-wide completeness. Gallery-dl may return only the ancestor chain when replies are behind nested show-more cursors; then the command adds nothing and known reply/stream-tip ids must be supplied explicitly. Fallback upload remains a separate explicit action after origin unavailability is confirmed.
|
||||
|
||||
Usual publish path after you like the note:
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@ from twitter.tw import (
|
||||
_copy_selected_media,
|
||||
_gallery_base_args,
|
||||
_merge_branch_posts,
|
||||
_merge_existing_capture,
|
||||
_merge_gallery_files,
|
||||
_select_branch_capture,
|
||||
_validate_capture_ids,
|
||||
build_parser,
|
||||
cmd_add_branch,
|
||||
cmd_emit,
|
||||
)
|
||||
|
||||
|
||||
@@ -333,6 +335,107 @@ class TwMediaCommandTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(SystemExit, "stored spine tip"):
|
||||
cmd_add_branch(Namespace(id="200", from_ids=["300"]))
|
||||
|
||||
def test_merge_existing_capture_keeps_fresh_and_retains_missing(self) -> None:
|
||||
old_100 = make_post("100", None)
|
||||
old_200 = make_post("200", "100")
|
||||
old_300 = make_post("300", "100")
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(old_100, old_200, old_300),
|
||||
source_url="https://x.com/i/status/100",
|
||||
)
|
||||
fresh_100 = replace(old_100, text="fresh root")
|
||||
fresh_200 = replace(old_200, text="fresh tip")
|
||||
fresh_400 = make_post("400", "200")
|
||||
fresh = ThreadData(
|
||||
root_post_id="200",
|
||||
posts=(fresh_100, fresh_200, fresh_400),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
merged, retained = _merge_existing_capture(fresh, existing, "200")
|
||||
self.assertEqual(
|
||||
merged.posts,
|
||||
(fresh_100, fresh_200, fresh_400, old_300),
|
||||
)
|
||||
self.assertEqual(merged.root_post_id, "200")
|
||||
self.assertEqual(merged.source_url, fresh.source_url)
|
||||
self.assertEqual(retained, ("300",))
|
||||
|
||||
def test_merge_existing_capture_rejects_different_conversation(self) -> None:
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(make_post("100", None),),
|
||||
source_url="https://x.com/i/status/100",
|
||||
)
|
||||
fresh = ThreadData(
|
||||
root_post_id="200",
|
||||
posts=(make_post("200", None),),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
with self.assertRaisesRegex(SystemExit, "different conversation"):
|
||||
_merge_existing_capture(fresh, existing, "200")
|
||||
|
||||
def test_emit_and_refresh_accept_preserve_existing(self) -> None:
|
||||
emit_args = build_parser().parse_args([
|
||||
"emit", "--id", "200", "--tip", "--preserve-existing",
|
||||
])
|
||||
refresh_args = build_parser().parse_args([
|
||||
"refresh", "--id", "200", "--tip", "--preserve-existing",
|
||||
])
|
||||
self.assertTrue(emit_args.preserve_existing)
|
||||
self.assertTrue(refresh_args.preserve_existing)
|
||||
|
||||
def test_emit_preserve_existing_merges_before_running_emitter(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
scratch = root / "refetch_200"
|
||||
assets = root / "assets"
|
||||
notes = root / "notes"
|
||||
notes.mkdir()
|
||||
existing = ThreadData(
|
||||
root_post_id="100",
|
||||
posts=(
|
||||
make_post("100", None),
|
||||
make_post("200", "100"),
|
||||
make_post("300", "100"),
|
||||
),
|
||||
source_url="https://x.com/i/status/100",
|
||||
)
|
||||
fresh = ThreadData(
|
||||
root_post_id="200",
|
||||
posts=(
|
||||
replace(make_post("100", None), text="fresh"),
|
||||
replace(make_post("200", "100"), text="fresh"),
|
||||
make_post("400", "200"),
|
||||
),
|
||||
source_url="https://x.com/i/status/200",
|
||||
)
|
||||
write_test_thread(assets / "thread_data.json", existing)
|
||||
write_test_thread(scratch / "thread_data.json", fresh)
|
||||
with (
|
||||
patch.object(tw_module, "scratch_dir", return_value=scratch),
|
||||
patch.object(tw_module, "dump_dir", return_value=root / "dump"),
|
||||
patch.object(tw_module, "locate", return_value=(assets, notes)),
|
||||
patch.object(tw_module, "locate_all", return_value=[]),
|
||||
patch.object(tw_module, "check_frozen"),
|
||||
patch.object(tw_module, "run") as run,
|
||||
redirect_stdout(StringIO()),
|
||||
):
|
||||
result = cmd_emit(Namespace(
|
||||
id="200",
|
||||
tip=True,
|
||||
slug=None,
|
||||
preserve_existing=True,
|
||||
))
|
||||
self.assertEqual(result, 0)
|
||||
self.assertTrue(run.called)
|
||||
merged = load_thread(scratch / "thread_data.json")
|
||||
self.assertEqual(
|
||||
tuple(post.post_id for post in merged.posts),
|
||||
("100", "200", "400", "300"),
|
||||
)
|
||||
self.assertEqual(merged.root_post_id, "200")
|
||||
|
||||
def test_migrate_defaults_to_dry_run(self) -> None:
|
||||
args = build_parser().parse_args(["migrate-media", "--id", "100"])
|
||||
self.assertEqual(args.cmd, "migrate-media")
|
||||
|
||||
+47
-1
@@ -320,6 +320,25 @@ def _merge_branch_posts(
|
||||
)
|
||||
|
||||
|
||||
def _merge_existing_capture(
|
||||
fresh: ThreadData,
|
||||
existing: ThreadData,
|
||||
tip_id: str,
|
||||
) -> tuple[ThreadData, tuple[str, ...]]:
|
||||
"""Keep fresh post data while retaining archived posts it omitted."""
|
||||
if tip_id not in {post.post_id for post in fresh.posts}:
|
||||
raise SystemExit(f"spine tip {tip_id} missing from fresh capture")
|
||||
fresh_roots = {
|
||||
post.post_id for post in fresh.posts if post.reply_to_id is None
|
||||
}
|
||||
existing_roots = {
|
||||
post.post_id for post in existing.posts if post.reply_to_id is None
|
||||
}
|
||||
if not fresh_roots or fresh_roots.isdisjoint(existing_roots):
|
||||
raise SystemExit("fresh and existing captures are a different conversation")
|
||||
return _merge_branch_posts(fresh, [existing.posts])
|
||||
|
||||
|
||||
def _copy_selected_media(
|
||||
source: Path,
|
||||
destination: Path,
|
||||
@@ -447,7 +466,12 @@ def cmd_add_branch(args: argparse.Namespace) -> int:
|
||||
}
|
||||
_copy_selected_media(capture_dir, output_dir, selected_added)
|
||||
_write_thread(output_dir / "thread_data.json", merged)
|
||||
cmd_emit(argparse.Namespace(id=post_id, tip=True, slug=None))
|
||||
cmd_emit(argparse.Namespace(
|
||||
id=post_id,
|
||||
tip=True,
|
||||
slug=None,
|
||||
preserve_existing=False,
|
||||
))
|
||||
|
||||
added_text = ",".join(added_ids) if added_ids else "-"
|
||||
leaf_text = ";".join(
|
||||
@@ -469,6 +493,23 @@ def cmd_emit(args: argparse.Namespace) -> int:
|
||||
src = dump_dir(post_id)
|
||||
if not (src / "thread_data.json").is_file():
|
||||
raise SystemExit(f"no thread_data.json for {post_id}")
|
||||
if args.preserve_existing:
|
||||
assets, _notes = locate(post_id)
|
||||
if assets is None:
|
||||
raise SystemExit(f"no existing emitted thread for {post_id}")
|
||||
existing_path = assets / "thread_data.json"
|
||||
if not existing_path.is_file():
|
||||
raise SystemExit(f"missing {existing_path}")
|
||||
fresh = load_thread(src / "thread_data.json")
|
||||
existing = load_thread(existing_path)
|
||||
merged, retained_ids = _merge_existing_capture(
|
||||
fresh,
|
||||
existing,
|
||||
post_id,
|
||||
)
|
||||
_write_thread(src / "thread_data.json", merged)
|
||||
retained = ",".join(retained_ids) if retained_ids else "-"
|
||||
print(f"preserve-existing retained={retained}")
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(HERE / "emit_archive.py"),
|
||||
@@ -795,6 +836,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
if name in ("emit", "refresh"):
|
||||
p.add_argument("--tip", action="store_true", help="use --id as tip")
|
||||
p.add_argument("--slug", default=None)
|
||||
p.add_argument(
|
||||
"--preserve-existing",
|
||||
action="store_true",
|
||||
help="retain archived posts omitted by the fresh capture",
|
||||
)
|
||||
if name == "lift":
|
||||
p.add_argument("--orig", action="store_true")
|
||||
if name == "ocr":
|
||||
|
||||
Reference in new issue
Block a user