Add the cherrypick-log Claude Code skill (.claude/skills/cherrypick-log): a script that lists yuzono and @secozzi commits not yet picked into this repo, with its config, docs and tests. yuzono commits that only touch src/ or lib-multisrc/ are left out unless they change at least 5 modules. Add cherrypick_log.md, generated against master and reviewed.
Upstream cherry-pick log
Tracks which upstream commits still need to be cherry-picked into this repo (yuzono/anime-extensions).
- Output:
cherrypick_log.mdin the repo root, a Markdown table with one row per upstream commit. - Generator:
update_cherrypick_log.py. This is deterministic Python 3.9+ that needs only the standard library andgit. - AI review:
SKILL.md, a Claude Code skill (/cherrypick-log). It runs the generator, sorts the repo-wide rows, and resolves the rows the generator couldn't decide (?) by comparing code. - Settings:
config.json.
The upstreams:
| Name | Branch | GitHub | What this repo takes from it |
|---|---|---|---|
yuzono (main) |
yuzono/main |
yuzono/tachiyomi-extensions |
The manga extensions repo, a copy of keiyoushi/extensions-source with its own commit hashes. Shared code only: core/, lib/, gradle/, .github/, CONTRIBUTING.md, build files. Its commits that only change src/ or lib-multisrc/ are left out unless they are repo-wide |
@secozzi (additional) |
@secozzi/master |
Secozzi/aniyomi-extensions |
Anime extensions this repo also ships (src/all/jellyfin, src/all/stremio, src/all/torbox). Every commit is listed |
The script decides everything that has a mechanical answer: the list itself, its order, the filters, matching
by hash or PR, and keeping manual marks. The AI handles only the judgment calls, and it writes through the
script's mark command, so the table can't be corrupted.
Quick start
# 1. Fetch upstreams and regenerate cherrypick_log.md (safe to run any time)
python3 .claude/skills/cherrypick-log/update_cherrypick_log.py
# 2. In Claude Code, let the AI sort the repo-wide rows and review the uncertain rows
/cherrypick-log review the log
# 3. Pick commits, then mark them yourself (or edit the Status / Notes cells by hand)
python3 .claude/skills/cherrypick-log/update_cherrypick_log.py mark 1a2b3c4d --status O
python3 .claude/skills/cherrypick-log/update_cherrypick_log.py mark 5e6f7a8b --status X --note "manga-only change"
The upstream remotes named in config.json must exist:
git remote add yuzono https://github.com/yuzono/tachiyomi-extensions and
git remote add @secozzi https://github.com/Secozzi/aniyomi-extensions. The script fetches them itself
unless you pass --no-fetch.
Commands
| Command | What it does |
|---|---|
update (default) |
Fetch upstreams, rebuild the list, keep manual marks and Notes. Options: --no-fetch, --dry-run, --verbose |
uncertain |
Print ? rows that haven't been AI-reviewed yet. Options: --include-empty, --include-reviewed, --repo NAME, --grep REGEX (title), --details REGEX (Details, e.g. '^repo-wide:'), --limit N, --json |
mark HASH |
Set one row's status (--status O|?|X|none) and/or append a note (--note, --replace-note). Refuses to change O/X rows unless you pass --force |
All commands accept --config PATH (default: config.json next to the script), --since YYYY-MM-DD
(overrides the config's start date for this run, e.g. ... --since 2024-01-01 update) and --target BRANCH (overrides
target_branch for this run, e.g. ... --target merge update to check a branch that is not merged into master yet).
They go before the command. Quote ? in zsh (--status '?').
Columns and statuses
| Column | Meaning |
|---|---|
| Status | O cherry-picked into the target branch · ? maybe, needs review (including commits that cancel each other out, see Revert chains) · empty (written as one space so the raw table stays aligned) = not found · X won't pick (set by hand, or a repo-wide row the AI review found not to apply, see Omitted commits) |
| Commit | Short hash, linked to the full GitHub commit. The script reads the full hash back from this link, so don't edit it |
| Upstream | yuzono, @secozzi, or ↳ @secozzi for @secozzi's copy of a yuzono commit (Details names it: pick of yuzono …) |
| Date | Committer date, i.e. when the commit landed on that upstream branch (ISO) |
| Title / Author | First line of the commit message and its author |
| Details | Generated on every run: why the row is listed (repo-wide: …), how it was paired and how its status was found (see below) |
| Notes | Yours. Kept across runs. AI notes start with AI checked <date>: |
Only Status and Notes are yours to edit. Every other cell, the order of the rows and the header are rewritten on each run.
Rules
Which commits are listed
- Non-merge commits on each upstream branch whose committer date is on or after
since. - A commit is excluded if its author, committer or any
Co-authored-bymatchesexclude_identity_regex(Renovate). A @secozzi copy of an excluded yuzono commit is excluded too. - A commit is excluded if every file it changes is under one of its upstream's
exclude_only_paths: here yuzono'ssrc/andlib-multisrc/, its manga extensions and themes. Renames count as a delete plus an add, so a file moved out of those paths keeps the commit. A commit that changes no files is kept. This is set per upstream: a secondary copy of an excluded primary commit is judged by its own files and its own upstream's setting. - Such a commit is kept anyway when it is repo-wide.
Repo-wide commits
One edit made across many sources can be a change this repo needs too, even though it only touches yuzono's
src/ and lib-multisrc/ (refactor UrlActivities, Use our ratelimit implementation, Remove usage of okhttp internal variables). So a commit dropped by exclude_only_paths is kept when it touches at least the upstream's
repo_wide_min_modules modules (here 5). Its Details start with repo-wide: N modules, only under src, lib-multisrc.
- A file's module is the directory before its first nested
src/,res/orassets/folder (Gradle / Android layout), else the file's directory. Sosrc/en/foo/src/…/Foo.ktandsrc/en/foo/build.gradleare bothsrc/en/foo, andlib-multisrc/madara/src/…islib-multisrc/madara. - Most commits that touch 5+ sources are bulk changes to yuzono's own sources: dead sources removed, domains updated,
a theme migrated. The AI review (SKILL.md section 2) sorts them. It marks the ones that are not repo-wide
X(noteAI checked <date>: omitted - not repo-wide (…)), and reviews the others like any row. Since 2025-01-01 that is about 200 rows for the first pass. Later updates add about 10 a month, more during migration waves (44 in 2026-07, the ext-lib 1.6 migration). - Raise
repo_wide_min_modulesfor fewer rows to sort, at the risk of missing small refactors. At 10 there are 87 rows, butRemove usage of okhttp internal variables(5 modules) is missed.0(or no key) turns this off.
Pairing @secozzi commits with yuzono commits
main_upstream in config.json is the primary one (yuzono here). Each entry of additional_upstreams
is a secondary (@secozzi here), and each secondary is paired with the primary on its own.
A secondary commit is a copy of a primary commit if one of these applies, checked in this order (shown as Details pick of yuzono … [method]):
marker: its message hascherry picked from commit <primary hash>.pr+title: its title has the same(#N)as a primary commit, and the titles are similar.title+author: the normalized title is identical, matches only one primary commit, and has the same author. The date window is one-sided: the secondary copy may land any time after the primary commit, but at most 30 days before it (PAIR_DATE_SLACK_DAYS; a copy can be picked from the PR before upstream merges it).
yuzono and @secozzi share no commits so far, so every @secozzi row is currently an own commit. The rules only come into play if @secozzi copies a yuzono commit.
If a @secozzi commit copies a yuzono commit dated before since, it is listed as a @secozzi-only commit, with (before <since>) in Details.
If it copies a yuzono commit dropped by exclude_only_paths (and is not dropped itself), Details say
(not listed: only changes under <paths>) instead.
Order (newest first)
All rows are sorted by committer time (when the commit landed on its upstream branch), newest first, whichever upstream they come from. So yuzono and @secozzi commits are interleaved by time.
- Commits with the same committer time keep their branch order, yuzono before @secozzi. This matters for yuzono: about 90% of its commits share their timestamp with other commits.
- Within one branch, time order and branch order agree (committer time never goes back along either branch), so the sort never reorders one upstream's own commits.
- A @secozzi copy of a yuzono commit is listed at its own time, not next to its original. Its Details name the
original (
pick of yuzono …), and the two rows share one status.
Status detection (shown as Details master `abc` [method])
A yuzono commit and its @secozzi copies share one status, and evidence for any of them counts for all of them.
| Method | Status | Meaning |
|---|---|---|
in history as … |
O | The upstream commit is reachable from the target branch |
hash of … |
O | A target commit's message has cherry picked from commit <hash> (or a github.com/…/commit/<hash> link) |
pr+title owner/repo#N |
O | A target commit line has owner/repo#N (or …/pull/N) and a similar title. Refs right after Fixes, Closes, See and similar words are ignored |
title+author |
O | A target commit line has the same normalized title and the same author |
pr only owner/repo#N |
? | The PR is referenced but the title differs |
title |
? | Same title, different author |
Title matches only count if the title is unique among the listed upstream commits, has at least 3 words,
and the target commit is at most 180 days older than the upstream commit.
The title is normalized before comparing: lowercased, conventional-commit prefix removed, (#N) refs and quotes removed.
Revert chains (flagged ?)
On every update, the script flags commits that cancel each other out as ?, with a
Cancels out (script): reverts / reverted by <hash> [was: …] note. You (or the AI review) decide what to do with them,
e.g. mark both X.
- They are found by following
This reverts commit <hash>links between listed commits. A yuzono commit and its @secozzi copies count as one. - A chain is only flagged when it is linear, every row in it is still empty or
?(this repo has none of them), and no row has an AI verdict. - An even-length chain cancels out completely, so every commit in it is flagged. In an odd-length chain (
C,Revert C,Revert "Revert C") the oldest commit carries the net change and is not flagged. - If this repo already has one side of a revert pair, the pair is not flagged, because the other side may still matter.
Older versions marked these rows X with an Omitted (script): note. Those rows are turned into ? flags on the next run.
Omitted commits (X)
X means "won't pick". Apart from your own marks, the AI skill marks one kind of row X: a repo-wide
row that turns out to be a bulk change to yuzono's own manga sources, not a change this repo needs. Its note is
AI checked <date>: omitted - not repo-wide (<what it changes>). See SKILL.md section 2.
Rerunning
- Rows with a status other than empty or
?(e.g.O, a manualX) are never changed. - A revert-chain flag (a
?with aCancels out (script):note, or an olderXwithOmitted (script):) is released on every run, back to the status recorded in its[was: …]part, and the chains are recomputed. So when a later revert extends a chain, the odd/even rule applies to the whole chain again. A flagged row is left alone once it has anAI checkednote, or once you change its status (e.g. toX). Only the stale script note is removed then. - Empty and
?rows are checked again. An empty or?row whose Notes containAI checkedkeeps that AI verdict unless newOevidence appears. - Notes are always kept, and new upstream commits are added in their place in the order.
- If a commit is no longer reachable from its upstream branch (force-push), it moves to a Gone from upstream section and keeps its status and notes. Delete it by hand once you no longer need it.
- Rows dated before
sincethat are already in the file stay at the end of the list (also sorted by time), with their cells unchanged. They come from a one-off pass over older history, which this repo hasn't had so far. An empty or?row there only changes toO, when newOevidence appears. You may delete such rows by hand; they are not re-added, and the summary simply counts the rows that are in the file. - If a commit is still upstream and dated on or after
since, but no longer matches the filters (e.g. you changedexclude_identity_regex,exclude_only_pathsorrepo_wide_min_modules), it is dropped. That run checks each dropped row with onegitcall, so it can take a minute when thousands of rows go.
Config (config.json)
| Key | Meaning |
|---|---|
target_branch |
Branch to check for already-picked commits (master). The local branch is read, so keep it up to date; --target overrides it |
since |
First commit date to include, YYYY-MM-DD (committer date). Required, and specific to each repo (here: 2025-01-01); --since overrides it |
output |
Output Markdown path, relative to the repo root (or absolute) |
exclude_identity_regex |
Case-insensitive regex matched against author, committer and co-authors (here: renovate). Empty = exclude nothing |
main_upstream |
{name, remote, branch, github} of the main upstream, plus the optional keys below. Copies in the other upstreams are paired with its commits. Required |
additional_upstreams |
List of one or more {name, remote, branch, github}, each with the optional keys below. Their commits are paired with the main upstream's. Names must be unique |
exclude_only_paths |
Per upstream, optional. List of repo-relative directories or files (leading / trailing / ignored). That upstream's commits that only change files under them are not listed. Here: ["src/", "lib-multisrc/"] on yuzono only, because @secozzi's src/ holds anime extensions this repo ships |
repo_wide_min_modules |
Per upstream, optional, whole number. A commit dropped by exclude_only_paths is kept as repo-wide when it touches at least this many modules. 0 or absent = off. Here: 5 on yuzono |
Tuning constants (similarity threshold, date limits, minimum title words, module folders) are at the top of the script.
Using this in another project
- Copy the whole
.claude/skills/cherrypick-log/folder. - Edit
config.json: themain_upstream, one or moreadditional_upstreams(remote, branch, GitHubowner/repo), target branch, start date (since, specific to that repo), excluded identities, excluded paths, the repo-wide threshold and output. - Add the remotes (
git remote add <name> <url>) and run the script. - Search
README.mdandSKILL.mdfor project-specific wording (yuzono, @secozzi, keiyoushi, manga / anime,src/andlib-multisrc/, the extension names,AGENTS.md) and adjust it.
The only hard requirement is that the fork records upstream picks with git cherry-pick -x
(cherry picked from commit …) or owner/repo#N references in commit titles.
Tests
python3 -m unittest discover -s .claude/skills/cherrypick-log -p 'test_*.py'
The tests in test_update_cherrypick_log.py cover revert-chain flags (including when a chain gets longer and the older X omissions), releasing
the script's own flags (including notes with escaped characters and user text), manual X pins, the rerun status rules
(carry_over: manual status kept, AI verdict kept unless O evidence appears), and the path filter (path matching,
modules, config checks, and Builder.build dropping path-only commits, keeping repo-wide ones and keeping a copy of a
dropped one), and the time order (reading committer time, interleaving upstreams, ties, copies at their own time).
They need no git repository.
Limitations
- These are heuristics. A reworded, split or hand-ported change can show as empty or
?. That's what the AI review is for. - yuzono's
(#N)refs arekeiyoushi/extensions-sourcePR numbers, and its hashes differ from keiyoushi's. A pick thatmasterrecords askeiyoushi/extensions-source#N, or with a keiyoushi hash, is only found through its title, so it can show?(e.g.Docs: URL intent filter (#14330)). Most such picks are still found through a yuzono hash or the title. - Repo-wide is a module count. A refactor that touches fewer than
repo_wide_min_modulesmodules under the excluded paths is dropped (e.g.fix gradle lint errors, 3 modules), and many bulk source changes are kept for the AI to sort. - Reverts are only linked through git's default
This reverts commit <hash>message, and only to commits that are in the list. A reworded revert, or a revert of a commit outside the list (beforesince, excluded, or deleted by hand), stays an ordinary row. - Revert chains are only flagged when they are linear, this repo has none of their commits and no row in them has an AI
verdict (
AI checked). Branching chains (two reverts of one commit) and pairs where this repo already has one side are not flagged. - One @secozzi commit that squashes several yuzono commits is a single row that shares its status with all of them (its Details name them).