In short: JavaScript's Map and Set compare object keys by identity, so two
equal vscode.Uris are different keys. HashMap and HashSet key on
toString() instead, and each has a read-only interface that the
Commit type uses.
commit_topology fills in every commit's ancestor and descendant sets and finds the working
copy and its parent. Merge holds a value that may be conflicted, as jj does.
dispose disposes everything in an array and empties it.
check.ts and time.ts just re-export the commit graph's own copies, which give
compile-time exhaustive switch checks and Mercurial-style "5 minutes ago" strings.
check and time are the only files nothing imports at this pin.
1. Files and who uses them
| File | Exports | Imported by |
|---|---|---|
| hashmap.ts | HashMap, ReadonlyHashMap, HashMapKey | ConflictTracker, keyed by document URI (tracker.ts, see merge-conflicts), SubprocessClient, hashset.ts and commit_topology.ts. |
| hashset.ts | HashSet, ReadonlyHashSet | api.ts, for Commit.ancestors and descendants (api.ts), SubprocessClient, the fakeCommit test fake, and the not-yet-wired MultiSelectManager. |
| commit_topology.ts | Six functions, see §4 | SubprocessClient.getCommits, which uses three of them (subprocess_client.ts). |
| merge.ts | Merge | api.ts, for CommitFile.mergedFileMetadata (api.ts), which nothing fills in yet. |
| dispose.ts | dispose | The UI bundle's dispose() (ui.ts) and the graph view's teardown when its webview is disposed (commit_graph_provider.ts). |
| check.ts | Everything in commit_graph/utils/check.ts | Nothing. The graph components import the original directly. |
| time.ts | Everything in commit_graph/utils/time.ts | Nothing. jj-time-ago-text imports the original directly. |
The re-exports exist because of Bazel visibility. //src/ui/commit_graph/utils is visible
only to //src/utils and to the commit graph's own packages
(BUILD.bazel).
//src/utils is public and depends on it (BUILD.bazel,
target), so it is the route for any other code to reach those
helpers.
2. HashMap
A thin wrapper over Map<string, [Key, Value]>. Every operation calls
key.toString() and uses the result as the real key. The original key object is stored next
to the value so keys() and entries() can hand it back
(source). Two keys are "equal" exactly when their strings are.
HashMapKey
| Member | Type | Meaning | Source |
|---|---|---|---|
toString | () => string | The identity of the key. It must be stable while the key is in the map, and different for keys that should differ. | L154 |
Methods
| Member | Type | Meaning | Source |
|---|---|---|---|
constructor | (iterable?: Iterable<[Key, Value]>) | Calls set for each pair. A later duplicate wins. | L48–54 |
set | (key, value) => void | Stores the pair. A replaced entry keeps its position in iteration order, but its stored key object becomes the new one. | L56–58 |
has, get, delete | (key) => … | As on Map, by string. | L60–74 |
keys, values | () => IterableIterator | Wrap the inner values() iterator and project the key or the value. | L76–82 |
entries | () => IterableIterator<[Key, Value]> | The stored tuples themselves, not copies. | L84–86 |
[Symbol.iterator] | () => IterableIterator<[Key, Value]> | Returns entries(), so the map works directly in for … of and can be passed to another map's constructor. | L88–90 |
clear | () => void | Empties the map. | L92–94 |
size | () => number | A method, unlike Map.size. | L96–98 |
putIfAbsent | (key, value) => Value | Returns the existing value if the key is present. Otherwise stores and returns the new one. | L104–111 |
ReadonlyHashMap is the same surface minus set, delete,
clear and putIfAbsent (source).
HashMap implements it, so a function can take a map it promises not to change, as
calculateAncestors does.
The two private iterator classes implement both next() and
[Symbol.iterator] over the same inner iterator (source).
for … of and spreading go through the generator. Calling next() by hand goes
through the method instead.
3. HashSet
A HashMap<Key, void> with set-shaped methods: add, has,
delete, clear, size(), keys(), and
[Symbol.iterator], so it can be used directly in for … of. The constructor adds
each element of an optional iterable (source). It has no
values, entries or forEach.
ReadonlyHashSet keeps only [Symbol.iterator], keys,
has and size (source). The public
Commit type exposes ancestors and descendants through it, and
MutableCommit maps each such field back to a writable HashSet so the client can
fill them in (api.ts).
4. commit_topology
Functions over a list of Commits (source). Upstream
says it was copied from Google's internal code with few changes, and added the
ancestors and descendants fields after it. Only the two calculate…
functions and getCommitIdToCommitMap are called, from the end of
SubprocessClient.getCommits; the other three are used only by their tests or not at all.
| Function | What it does | Source |
|---|---|---|
getWorkingCopyCommit(commits) | The commit with active set. Throws, through logAndShowInternalError, if there is none. | L22–32 |
getOnlyWorkingCopyParentCommit(commits, wc) | The working copy's parent when it has exactly one and that parent is in the list. Throws if it has no parents; returns undefined for a merge. | L34–53 |
getCommitIdToCommitMap(commits) | A HashMap from commit ID to commit. | L55–66 |
calculateAncestors(commitMap) | Fills each commit's ancestors: itself, plus the ancestors of every parent. See below. | L68–115 |
calculateDescendants(commitMap) | The mirror image over childIds: itself, plus the descendants of every child. | L117–165 |
getChangeIdToCommitsMap(commits) | A HashMap from change ID to every commit with it, in list order. More than one entry means the change is divergent. | L167–183 |
getEvolvedCommit(commits, original) | Follows a remembered commit across a refresh. Still present by commit ID: itself. Otherwise the one commit with its change ID. Gone, or divergent: undefined, and the comment says the caller then falls back to the working copy's parent. | L185–219 |
Both calculate… functions are an iterative post-order depth-first search, so a long
history can't overflow the call stack. For each commit they keep an explicit stack. A commit whose set
is already non-empty is finished and is popped. Otherwise every parent that is in the map and not yet
finished is pushed, and the commit is revisited later. Once none are pushed, the commit's set becomes
itself plus the union of its parents' sets, so each set is computed once and reused, whatever order
the map iterates in. A parent ID that isn't in the map, such as a public base outside the revset, is
added as-is but not followed. A non-empty set doubles as the "visited" mark, which works because every
finished set contains at least the commit itself.
5. check: exhaustiveness helpers
Re-exported unchanged from the commit graph (source).
Each helper takes a value whose type should already have been narrowed to never (or to an
explicitly allowed type). Adding a new enum or union member then turns every unhandled
switch into a compile error.
| Member | Type | Meaning | Source |
|---|---|---|---|
checkExhaustive | (value: never, msg?) => never | Compile-time check that always throws at runtime, with unexpected value …! by default. Used in the graph's row title, lines and drag-and-drop code. | L63–65 |
checkExhaustiveAllowing | <Allowed>(value, msg?) => never | The same, but values of Allowed also type-check, for enum members that "can't happen". | L97–102 |
LegacyIncorrectUsage | unknown | Placeholder type argument for old call sites. Unused here. | L115 |
assumeExhaustive, assumeExhaustiveAllowing | (value) => void | Compile-time check only. A no-op at runtime, for switches that fall back to a default instead of failing. | L140–168 |
The file's comments point to an internal "go/guards-and-assertions" library. It was carried over from Google's internal TypeScript tooling and has no public counterpart here.
6. dispose
dispose(disposables) calls dispose() on each element in order, then sets the
array's length to 0 so the same array can be reused or disposed again harmlessly
(source). If one dispose() throws, the loop stops: the
remaining elements aren't disposed and the array isn't emptied.
7. time: relative timestamps
age(unixSeconds) deliberately mirrors Mercurial's age template filter, so the
graph reads like hg xl (source). The graph webview
shows it through jj-time-ago-text (graph-webview).
- The exact same second is
now. - More than 30 years in the future is
in the distant future. More than two years in the past isDate.toDateString(), for exampleTue Mar 03 2020. - Otherwise it picks the largest unit that fits at least twice: years of 365 days, months of 30 days,
weeks, days, hours, minutes, seconds (table). So 90 seconds
is
90 seconds ago, and 13 days is13 days ago, not1 week ago. A future date gets… from now.
8. Tests and build
| Test | Checks | Source |
|---|---|---|
| HashMap finds the value | A different instance with the same string finds and overwrites the entry. | 28–37 |
| HashMap iterates correctly | keys, values and entries each yield the three entries. | 39–83 |
| HashMap [Symbol.iterator] works correctly | for … of over the map yields the three pairs. | 85–112 |
| HashMap putIfAbsent works correctly | The second call returns the first value. | 114–118 |
| HashSet finds elements correctly | Membership by string. | 30–34 |
| HashSet iterates correctly | for … of yields three keys. | 36–50 |
| getOnlyWorkingCopyParentCommit (3 tests) | Parent missing from the list, one parent, a merge. | 32–76 |
| getEvolvedCommit (2 tests) | Unchanged commit; a rewritten commit found by change ID. | 78–105 |
| calculateAncestors (7 tests) | A root, a chain, a diamond with no duplicates, a parent outside the map, no leakage to children or sibling branches, reuse of a parent's set, and a deep chain without stack overflow. | 107–273 |
| calculateDescendants (7 tests) | The same seven cases, mirrored over children. | 275–441 |
| Merge fromResolved, fromAlternatingAddsAndRemoves, fromAddsAndRemoves (8 tests) | A merge always has one more add than removes, the two list constructors throw otherwise, and fromAddsAndRemoves copies its input arrays. | 21–110 |
| Merge simplify (7 tests) | simplify cancels each add that equals a remove, in place, by a caller-supplied equality, so a multi-way conflict whose removes all cancel becomes resolved. | 112–185 |
time.ts has its tests next to the original in src/ui/commit_graph/utils/.
dispose, check, getWorkingCopyCommit,
getCommitIdToCommitMap and getChangeIdToCommitsMap have none of their own. The
topology tests build commits with the fakeCommit fake from //src/testing.
hashmap, hashset, merge and commit_topology are now their own
ts_library targets, "to avoid circular dependencies with api.ts": api.ts needs
HashSet and Merge, and commit_topology needs //src/api
(BUILD.bazel). The utils target globs the rest of
*.ts, excluding those four, and the ts_library macro drops the
*_test.ts files from it, so they compile only into the tests target
(BUILD.bazel).
9. Loose ends at this pin
- Unused exports. The
checkandtimere-exports have no importers, andgetWorkingCopyCommit,getOnlyWorkingCopyParentCommit,getChangeIdToCommitsMapandgetEvolvedCommithave no callers outside tests. toStringis not checked. Every object has one, so the type system accepts a key whosetoStringis the default[object Object]. All such keys would collapse into one entry.vscode.Uri, the only key type used, has a real one.ValueIterator.next()at the end. It readsnext.value[1]without the?.thatKeyIteratorhas, so callingnext()by hand past the last value throws aTypeError(source).for … ofisn't affected.- Shared tuples.
entries()yields the stored[key, value]arrays, so a caller that mutates one changes the map without updating its string key. HashSet's iterator comment still calls it "the ImmutableSet" (source).