Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Central Test Suite

This is central’s own test suite, and also its book: pages are generated from OCaml source files (via mdexp) that are also real, running dune runtests, so the narrative prose, the code, and the snapshots you see embedded in a page are never allowed to drift from what actually happens when the code runs.

Layout

  • expect/ holds every test file, both the literate, book-generating ones (each carrying @mdexp directives and a generated .md counterpart, checked in next to the .ml - see SUMMARY.md for the current list of pages) and the plain ones (test__central.ml).
  • gitrepo/ and gitrepo-file-parser/ test the .gitrepo file parser in isolation.

This is part of the effort to move central’s subrepo workflow off git-subrepo and onto small, dedicated pieces of OCaml logic: each command gets a page here explaining what it does and demonstrating it end to end. The intent is for this book to grow to cover central more broadly, not just the subrepo commands.

How does it relate to the user documentation?

There is inevitable overlap between this test book and the documentation in doc/. The key difference is intent:

  • doc/ is focused on the user experience - how to install, configure, and use central. It omits low-level details.
  • test/ is focused on correctness - every guardrail, every error case, every CLI invocation. It includes details that would overwhelm a user guide but are essential for someone modifying the code.

When both cover the same topic, the doc version explains what to do while the test version proves that it works.

Building

dune runtest

regenerates the book’s pages from their source .ml files, and runs every other test in the tree. To browse the book itself:

cd test && mdbook serve --open

A day-to-day workflow

This walks through the everyday loop of working in central: edit something, check central todo for what needs attention, act on it, and confirm the dashboard is clear again.

central todo’s dashboard covers every subrepo central knows about, so this fake repo (unlike the one on the Export page) is built with a few of them, not just widget - enough to show the dashboard correctly narrows down to only what needs attention.

With nothing out of the ordinary going on, the dashboard is empty:

$ central todo

Editing directly in central

Suppose someone edits repo/widget/README.md directly from within central and commits it there - the way most day-to-day changes happen.

central todo now shows widget needs attention:

$ central todo
┌──────────┬───────────┬──────┐
│ Repo     │ Next step │ Diff │
├──────────┼───────────┼──────┤
│ central  │ push      │    2 │
│   widget │ export    │    2 │
└──────────┴───────────┴──────┘

Following it means exporting:

$ central export widget -m "Document installation"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].

widget’s row is still there, but the next step changed: export only advances the subrepo’s subrepo branch, so widget’s own main branch is now behind it. This is a genuine second step, not a leftover of the first:

$ central todo
┌──────────┬──────────────┬──────┐
│ Repo     │ Next step    │ Diff │
├──────────┼──────────────┼──────┤
│ central  │ push         │    6 │
│   widget │ advance-main │      │
└──────────┴──────────────┴──────┘

advance-main is exactly that: catch widget’s main branch up:

$ central advance-main widget
==================== widget ====================
Updating 1185512..f452a6f
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

widget is left with a push next step, same as central itself: both now have local commits their own remote doesn’t have yet - central’s edit and the .gitrepo update export made, and the commit advance-main just fast-forwarded widget’s own main to:

$ central todo
┌──────────┬───────────┬──────┐
│ Repo     │ Next step │ Diff │
├──────────┼───────────┼──────┤
│ central  │ push      │    6 │
│   widget │ push      │      │
└──────────┴───────────┴──────┘

push closes the loop for both at once - a real git push to each one’s own remote:

$ central push central widget --yes
==================== central ====================
[ OK ] Pushed.
==================== widget ====================
[ OK ] Pushed.

And the dashboard is clear again - back where we started, the change now genuinely out, all the way to both real remotes:

$ central todo

Export

central export <repo> -m MSG moves changes made directly in central, under repo/<repo>/, out into <repo>’s own standalone git repository - as a single new commit on top of its subrepo branch.

It is the native-OCaml replacement for the one git subrepo command that sits on central’s critical, day-to-day path: git subrepo push. Unlike that command, export always squashes whatever changed since the last sync into exactly one new commit, using the message supplied with -m.

Concretely, it:

  1. Verifies the subrepo’s subrepo branch has not moved since the last sync recorded in repo/<repo>/.gitrepo (if it has, someone likely pushed there directly - bring that in first before exporting).
  2. Computes the diff of everything under repo/<repo>/ since that sync, using git diff --relative=, which also strips the repo/<repo>/ path prefix from the patch.
  3. Applies that patch as one commit onto the subrepo’s subrepo branch, via git apply --3way --index.
  4. Updates .gitrepo in central to record the new sync point.

A fake repo to work with

This walkthrough uses Central_test_helpers to build a fake central repo with a made-up subrepo, widget, and a fake standalone repo standing in for widget’s own history, wired together with a .gitrepo file exactly as central subrepo init would leave them.

Right after that, repo/widget/README.md in central and widget’s own README.md are identical:

# widget

This is a fake [widget] repo, generated by [Central_test_helpers] for tests.

Editing directly in central, then exporting

Suppose someone edits repo/widget/README.md directly from within central and commits it there - the way most day-to-day changes happen. Running export then brings that change out, as a single new commit on top of widget’s subrepo branch:

$ central export widget -m "Document installation"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].

It landed in widget’s own history as one new commit on the subrepo branch, carrying exactly that change:

Document installation
Initial commit
# widget

This is a fake [widget] repo, generated by [Central_test_helpers] for tests.

Added a line about installation.

Guardrails

Running export again right away, with nothing new to export, is a clean error rather than an empty commit:

$ central export widget -m "Nothing changed"
==================== widget ====================
Error: Nothing to export: no changes under "repo/widget" since the last sync.
[123]

And if the subrepo’s subrepo branch moved since the last sync - for example because someone pushed to it directly, bypassing central - export refuses rather than silently basing the new commit on the wrong parent:

$ central export widget -m "Should not apply"
==================== widget ====================
Error: The [subrepo] branch of [widget] has moved since the last sync
recorded in [.gitrepo].
Hint: Bring those changes into central first with [central import] before
exporting, or pass [--force] to export anyway.
[123]

And as a precondition, export first checks that central’s own working tree is clean - an unstaged edit is rejected outright, before anything else is even looked at:

$ central export widget -m "Should not apply"
==================== widget ====================
Error: Repo "$CENTRAL_ROOT" has uncommitted changes.
M repo/widget/README.md
Hint: Commit or stash them first.
[123]

Exporting several subrepos at once

Chores that make the same systematic change across many subrepos are common enough that export accepts more than one REPO at a time (or --all for every subrepo it knows about) - each is exported in turn, in the order given, with a separator banner between them, reusing the same -m message for every commit:

$ central export widget gadget -m "Add license footer"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].
==================== gadget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [gadget].

Both landed the same commit message on their own subrepo branch:

-- widget --
Add license footer
Initial commit
-- gadget --
Add license footer
Initial commit

central todo picks up both subrepos as done - since export doesn’t touch central’s own history, both are shown ready for their next step, advance-main, while central itself needs a push for the .gitrepo updates:

$ central todo
┌──────────┬──────────────┬──────┐
│ Repo     │ Next step    │ Diff │
├──────────┼──────────────┼──────┤
│ central  │ push         │   12 │
│   gadget │ advance-main │      │
│   widget │ advance-main │      │
└──────────┴──────────────┴──────┘

export stops at the first repo that fails, leaving the ones after it in the list untouched - it doesn’t try to skip ahead and report a summary at the end. Say gadget’s subrepo branch moved independently in the meantime (someone pushed to it directly, bypassing central) while widget is perfectly exportable:

$ central export widget gadget -m "Add license footer"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].
==================== gadget ====================
Error: The [subrepo] branch of [gadget] has moved since the last sync
recorded in [.gitrepo].
Hint: Bring those changes into central first with [central import] before
exporting, or pass [--force] to export anyway.
[123]

Import

central import <repo> brings commits from the tip of the subrepo’s subrepo branch - normally landed there by fetching from the subrepo’s real remote - into repo/REPO. -m MSG is optional and defaults to "Import changes from REPO".

Unlike export, this has to account for central having moved on since the last sync - so it picks between two ways of bringing the change in, depending on whether central has any local changes of its own under repo/REPO:

  • If central hasn’t touched repo/REPO at all since the last sync, repo/REPO at central’s current HEAD is - by construction - exactly what it was at the last sync point, so the subrepo’s diff is guaranteed to apply there too. There is nothing to merge, so import doesn’t build one: it applies the diff directly as a single new commit on top of HEAD. This is the default, and it keeps history linear in the common case where central had no reason to conflict with what the subrepo brings in.
  • Otherwise - central does have local changes of its own under repo/REPO - import falls back to an ordinary two-parent git merge: it builds a new commit as a direct child of the central revision recorded in repo/REPO/.gitrepo - not of the current HEAD - and applies the subrepo’s diff there, where it is guaranteed to apply cleanly regardless of what else happened on central’s actual HEAD since. That commit (the “import commit”) also updates .gitrepo to record the new sync point. Merging it into the active branch is then an ordinary git merge: if there is a real conflict, git surfaces it exactly as it always does, for a human to resolve.

Applying directly

A fake central and a fake widget, as usual - someone pushes directly to widget’s subrepo branch (standing in for something fetched from its real remote), while central hasn’t touched repo/widget/ since the last sync. So import applies the upstream change straight onto HEAD, no merge needed:

$ central import widget
[ OK ] Imported into [main] directly (no merge needed).

A single, linear commit lands directly on top of HEAD - no second parent, no merge commit. -m was left out here, so the message falls back to "Import changes from widget":

Import changes from widget
Add fake subrepo widget
Initial commit
# widget

This is a fake [widget] repo, generated by [Central_test_helpers] for tests.

Edited directly upstream.

Guardrails

Running import again right away, with nothing new upstream, is a clean error rather than an empty commit - symmetric to export’s own guard:

$ central import widget
Error: Nothing to import: the [subrepo] branch of [widget] has not moved
since the last sync.
[123]

And just like export, import first checks that central’s own working tree is clean - an unstaged edit is rejected outright:

$ central import widget
Error: Repo "$CENTRAL_ROOT" has uncommitted changes.
M README.md
Hint: Commit or stash them first.
[123]

Falling back to a merge

This time, central does touch something under repo/widget/ since the last sync - a new file of its own, NOTES.md, unrelated to the file upstream changed. import can no longer assume repo/widget/ is untouched, so it falls back to building an import commit as a child of the last sync point and merging it in. Since the two sides touch different files, the merge still completes cleanly on its own:

$ central import widget
[ OK ] Built the import commit.
Merge made by the 'ort' strategy.
 repo/widget/.gitrepo  | 4 ++--
 repo/widget/README.md | 2 ++
 2 files changed, 4 insertions(+), 2 deletions(-)
[ OK ] Imported into [main].

Unlike the direct case above, the import commit sits as a child of the old sync point, not of central’s HEAD at the time - a separate line of history, joined by the merge:

Merge widget import
Add local notes under widget
Add fake subrepo widget
Initial commit

A real conflict

Same fallback, but this time central and the subrepo both edit the very same line under repo/widget/ since the last sync:

$ central import widget -m "Bring in upstream retitle"
[ OK ] Built the import commit.
Auto-merging repo/widget/README.md
CONFLICT (content): Merge conflict in repo/widget/README.md
Automatic merge failed; fix conflicts and then commit the result.
Error: Merge conflict while importing - resolve the conflicts above in
[main], then [git add] the resolved files and [git commit] to finish the
merge.
Hint: .gitrepo has already been updated as part of the import commit being
merged - no further action needed there once the merge is complete.
[123]

Central is left in the middle of the merge, exactly as an ordinary git merge would - conflict markers included, with the trailing revision on >>>>>>> naming the import commit, the side being merged in. That revision is a deterministic mock rather than the real sha - see Deterministic Revisions for why:

<<<<<<< HEAD
# widget, edited by central
=======
# widget, retitled upstream
>>>>>>> 1185512b92d612b25613f2e5b473e5231185512b

Resolving it is the same as for any git merge conflict - pick a resolution, git add, git commit:

Resolve README retitle conflict
Central retitles the README
Add fake subrepo widget
Initial commit

.gitrepo was already updated as part of the import commit, so export is available again right away - it carries the resolution itself out to widget, since that’s what central’s own history now disagrees with:

$ central export widget -m "Resolve conflicting retitle"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].
Resolve conflicting retitle
Upstream retitles the README
Initial commit

Stitch

central stitch <repo> is for a narrower situation than import: someone exported a change, then reworked the commit(s) that just landed in the subrepo’s own history - splitting one commit into a nicer sequence, squashing, reordering, rewording - without changing the tree they arrive at. repo/<repo>/.gitrepo in central still names the pre-rewrite commit, which no longer exists on the subrepo’s subrepo branch.

Running import at this point would either fail outright (the old commit isn’t an ancestor of the new tip any more) or, if it somehow went through, apply an empty patch for no reason - there is nothing to actually bring in, since the tree hasn’t changed. stitch is the narrow fix: it just repoints .gitrepo at the subrepo’s new tip, and commits that update with an auto-generated message - central’s copy of a subrepo isn’t public history, so unlike export there’s nothing worth writing by hand here.

The required pre-conditions:

  1. The subrepo branch has actually moved since the last sync.
  2. There is no real content diff between the commit recorded in .gitrepo and the subrepo’s current tip - i.e. this really is a pure history rewrite.
  3. Central has no local changes of its own under repo/<repo>/ since the last sync.

Rewriting history after an export

Suppose a change lands in central and gets exported as usual, as one squashed commit:

$ central export widget -m "Document feature A and B"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].

Now, imagine that squashed commit gets reworked directly in widget’s own history into two smaller, better organized commits - reaching the exact same final README.md either way. .gitrepo still names the abandoned squash commit, which no longer exists on subrepo:

import would refuse here - the commit .gitrepo names is gone, so it can’t tell this apart from a more troubling rewrite. stitch recognizes it for what it is and just catches .gitrepo up, committing the update itself with an auto-generated message:

$ central stitch widget
==================== widget ====================
[ OK ] Stitched [widget].
Stitch repo widget
export widget
Document feature A and B
Add fake subrepo widget
Initial commit

Running stitch again right away is a clean error - .gitrepo is already caught up, so there is nothing left to stitch:

$ central stitch widget
==================== widget ====================
File "$CENTRAL_ROOT/repo/widget/.gitrepo", line 1, characters 0-0:
Error: Nothing to stitch: the [subrepo] branch of [widget] is already the
commit recorded in [.gitrepo].
[123]

And central todo confirms both sides agree - widget’s own main just needs to catch up, same as after any ordinary export:

$ central todo
┌──────────┬──────────────┬──────┐
│ Repo     │ Next step    │ Diff │
├──────────┼──────────────┼──────┤
│ central  │ push         │    8 │
│   widget │ advance-main │      │
└──────────┴──────────────┴──────┘

Guardrails

stitch only repoints .gitrepo - it never brings in real content changes. If the subrepo’s tip actually differs in substance from what .gitrepo records, this isn’t a pure history rewrite any more, and stitch refuses rather than silently pretending the trees still match:

$ central stitch widget
==================== widget ====================
File "$CENTRAL_ROOT/repo/widget/.gitrepo", line 1, characters 0-0:
Error: Nothing to stitch: the [subrepo] branch of [widget] is already the
commit recorded in [.gitrepo].
[123]
$ central stitch widget
==================== widget ====================
File "$CENTRAL_ROOT/repo/widget/.gitrepo", line 1, characters 0-0:
Error: Cannot stitch: "repo/widget" has content changes between the commit
recorded in [.gitrepo] and its current tip - this isn't a pure history
rewrite.
Hint: Use [central import] instead to bring those changes in.
[123]

And if central itself has moved on with local changes of its own under repo/<repo>/ since the last sync - even alongside an otherwise legitimate history rewrite upstream - stitch refuses too, since a plain re-pointing of .gitrepo can no longer account for the full picture:

$ central export widget -m "Document feature A"
==================== widget ====================
[ OK ] Applied patch in the subrepo.
[ OK ] Exported to [widget].
$ central stitch widget
==================== widget ====================
File "$CENTRAL_ROOT/repo/widget/.gitrepo", line 1, characters 0-0:
Error: Cannot stitch: central has local changes of its own under
"repo/widget" since the last sync.
Hint: Export or import those changes first, then stitch.
[123]

And as a precondition, stitch first checks that central’s own working tree is clean - an unstaged edit is rejected outright, before anything else is even looked at:

$ central stitch widget
Error: Repo "$CENTRAL_ROOT" has uncommitted changes.
M README.md
Hint: Commit or stash them first.
[123]

Push

central push <repo>... pushes the given repo’s (or repos’) main branch to its real remote - the final step once a change has made its way all the way to a subrepo’s own main (via export, then advance-main if needed), or simply for central’s own local commits.

By default, before pushing, it opens gitk --all to visualize the history and asks for confirmation; every example below passes --yes to skip both, the way this would run non-interactively (e.g. from a script or CI).

Nothing to push

Right after Central_test_helpers.create, every repo’s main is already up to date with its own remote - pushing is a clean no-op:

$ central push central --yes
==================== central ====================
[SKIP] Skipping [push] (not applicable).

Pushing central’s own commits

Once central has a local commit its remote doesn’t have yet, push is applicable, and sends it there. Here, that commit comes from importing an upstream change from widget - the everyday way central ends up with something to push:

$ central import widget
[ OK ] Imported into [main] directly (no merge needed).
Add fake subrepo widget
Initial commit
$ central push central --yes
==================== central ====================
[ OK ] Pushed.

The commit really is on the remote now - reading its log directly (rather than trusting central’s own say-so) confirms it:

Import changes from widget
Add fake subrepo widget
Initial commit

Pushing a subrepo

The same, for a subrepo’s own main - as it would be after a change went through export (and advance-main, catching main up to the subrepo branch export landed on). Here, to isolate what push itself does, main gets a commit directly:

$ central push widget --yes
==================== widget ====================
[ OK ] Pushed.
Direct edit in widget
Initial commit

--dry-run/interactive mode (the default) opens gitk --all to preview the history before confirming - which needs a real display and isn’t something this book can exercise deterministically. The “not applicable” guard is checked before the preview, though, so that much is safe to demonstrate regardless of confirm mode:

$ central push central --dry-run
==================== central ====================
[SKIP] Skipping [push] (not applicable).

Advance Main, Advance Subrepo

export only ever moves a subrepo’s subrepo branch - it never touches main, so main falls one commit behind after every export. Two commands catch it back up, at different scopes:

  • central advance-main <repo> fast-forwards that subrepo’s local main branch to match subrepo, from the same machine an export (or import) just ran on - the everyday, single-machine case.
  • central advance-subrepo <repo> is the more thorough version, aimed at a second machine: after pulling central’s own main (which brings in whatever .gitrepo now says), it fast-forwards that subrepo’s local subrepo and main branches to match - both in one go, provided the commit .gitrepo points to is already present locally (e.g. already fetched from the subrepo’s real remote - advance-subrepo itself never fetches anything).

Advance-main, right after an export

export lands a commit on widget’s subrepo branch - main hasn’t moved:

$ central advance-main widget
==================== widget ====================
Updating 1185512..f452a6f
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

main is fast-forwarded to the same commit subrepo already carried:

Document installation
Initial commit

Right after Central_test_helpers.create, there’s nothing for main to catch up to:

$ central advance-main widget
==================== widget ====================
[SKIP] Skipping [advance-main] (not applicable).

Advance-subrepo, catching up a fresh checkout on another machine

Say a change was already exported and pushed from elsewhere: central’s own .gitrepo (as pulled from its real remote) now points at a newer widget commit, already fetched into this machine’s own checkout of widget (e.g. by a plain git fetch, run once ahead of time), but not yet merged into either its subrepo or main branch. advance-subrepo brings both up to date in one command:

$ central advance-subrepo widget
==================== widget ====================
Updating f452a6f..1185512
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)
Updating f452a6f..1185512
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

subrepo is on the new commit now:

Document installation
Initial commit

And so is main - both branches now point at the very same commit:

Document installation
Initial commit

Todo

central todo is the dashboard: one row for central itself, plus one row per subrepo that has something outstanding - each with its Next step (what central command to run next) and, for subrepos, Diff (how many lines under repo/<name>/ differ from what’s already been dealt with). Subrepos with nothing outstanding simply don’t appear, so the table only ever shows what actually needs attention - see the day-to-day workflow for it used end to end.

central’s own row uses Repo_config.root_repo_name as its label - "central" by default, but configurable per-repo (see config.md).

Once repo/widget/ has an uncommitted-to-upstream change, widget shows up with export as its next step, and central itself already needs a push for the commit that introduced it:

After export, widget’s next step becomes advance-main - the Diff column goes blank, since there’s no longer a content diff to size, only a branch to fast-forward:

And with a .central/repo-config.json setting a custom name, that name - not "central" - is what labels the top row:

Config

central reads two small, optional JSON config files, each with a $schema under schema/ for editor support:

  • Repo_config - read from .central/repo-config.json, at the root of the monorepo itself. Currently just name, the string that identifies “the central repo” among the positional arguments to commands like push and todo (as opposed to a subrepo) - defaults to "central".
  • User_config - read from the XDG config directory, ~/.config/central/user-config.json. Empty for now; a placeholder for per-user settings to come.

Both are entirely optional: a missing file just means the default.

Repo_config.find_and_load is what commands actually call: it looks for .central/repo-config.json in the repo and falls back to the default if it isn’t there.

Unknown fields are rejected rather than silently ignored - a typo in the config file is a loud error, not a silently-dropped setting:

User_config follows the same shape, currently an empty record:

Subrepo

Central.Subrepo.t identifies one of the sub-repos vendored under repo/ in the enclosing monorepo. It isn’t a fixed, hand-maintained enum: it’s just a validated string (the directory name under repo/), and the set of known sub-repos is discovered dynamically by walking the filesystem.

Discovery

all walks repo/’s direct children and keeps the ones that contain a .gitrepo file, sorted by name:

find_on_disk looks a sub-repo name up against all - unlike of_string, it validates that the name actually names a vendored sub-repo, not merely that it has the right shape:

Manipulating paths

Unlike all and find_on_disk above, everything in this section is pure path manipulation: none of it reads the filesystem, or confirms that anything it is given actually exists on disk.

root and gitrepo_file_path locate a sub-repo’s own directory, and its .gitrepo file, as paths in the enclosing monorepo:

central_path and subrepo_path convert a path back and forth between the two frames of reference a path can be expressed in: relative to the sub-repo’s own root (what the sub-repo’s standalone checkout sees), or relative to the enclosing monorepo (prefixed with root, what the monorepo checkout sees):

repo/widget/src/dune

subrepo_path returns None for a path that doesn’t belong to the sub-repo at all - and, since a path in the sub-repo’s own repo is never empty, for the sub-repo’s root itself:

Deterministic Revisions

Every command that touches git prints real revisions - commit hashes, MERGE_HEAD, the trailing >>>>>>> <sha> on a conflict marker - and a real revision is different every time a test runs, since it’s derived from tree content, parents, and commit timestamps, none of which stay fixed between runs. Left alone, that would make every snapshot in this book flaky.

Central_test_harness fixes this by redacting: every real revision it sees is auto-detected and rewritten to a deterministic mock counterpart (see the conflict example in Import for one in the wild), so the same command run today or a year from now prints the exact same snapshot. to_mock_rev/register_rev map a real revision to its mock; redact applies that mapping - and every abbreviated prefix git might plausibly print for it - to a piece of text.

A mock revision is itself just a 40-character hex string, indistinguishable from a real one. That has one sharp edge worth pinning down directly: a mock revision can coincidentally contain a run of characters equal to some other, unrelated revision’s abbreviated prefix. redact has to substitute every registered revision starting from the original text in a single pass, so that a mock revision it has already written out is never handed back for re-examination - otherwise a later, shorter pattern could match inside it and corrupt it:

1185512b92d612b25613f2e5b473e5231185512b