| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {RecordOf} from 'immutable'; |
| b69ab31 | | | 9 | import type {Hash} from '../types'; |
| b69ab31 | | | 10 | import type {ExtendedGraphRow} from './render'; |
| b69ab31 | | | 11 | import type {SetLike} from './set'; |
| b69ab31 | | | 12 | |
| b69ab31 | | | 13 | import {Map as ImMap, Set as ImSet, List, Record} from 'immutable'; |
| b69ab31 | | | 14 | import {cachedMethod, LRU} from 'shared/LRU'; |
| b69ab31 | | | 15 | import {SelfUpdate} from 'shared/immutableExt'; |
| b69ab31 | | | 16 | import {group, notEmpty, nullthrows, splitOnce} from 'shared/utils'; |
| b69ab31 | | | 17 | import {CommitPreview} from '../previews'; |
| b69ab31 | | | 18 | import {BaseDag, type SortProps} from './base_dag'; |
| b69ab31 | | | 19 | import {DagCommitInfo} from './dagCommitInfo'; |
| b69ab31 | | | 20 | import {MutationDag} from './mutation_dag'; |
| b69ab31 | | | 21 | import {Ancestor, AncestorType, Renderer} from './render'; |
| b69ab31 | | | 22 | import {TextRenderer} from './renderText'; |
| b69ab31 | | | 23 | import {arrayFromHashes, HashSet} from './set'; |
| b69ab31 | | | 24 | |
| b69ab31 | | | 25 | /** |
| b69ab31 | | | 26 | * Main commit graph type used for preview calculation and queries. |
| b69ab31 | | | 27 | * |
| b69ab31 | | | 28 | * See `BaseDag` docstring for differences with a traditional source |
| b69ab31 | | | 29 | * control dag. |
| b69ab31 | | | 30 | * |
| b69ab31 | | | 31 | * A commit is associated with the `Info` type. This enables the class |
| b69ab31 | | | 32 | * to provide features not existed in `BaseDag`, like: |
| b69ab31 | | | 33 | * - Lookup by name (bookmark, '.', etc) via resolve(). |
| b69ab31 | | | 34 | * - Phase related queries like public() and draft(). |
| b69ab31 | | | 35 | * - Mutation related queries like obsolete(). |
| b69ab31 | | | 36 | * - High-level operations like rebase(), cleanup(). |
| b69ab31 | | | 37 | */ |
| b69ab31 | | | 38 | export class Dag extends SelfUpdate<CommitDagRecord> { |
| b69ab31 | | | 39 | constructor(record?: CommitDagRecord) { |
| b69ab31 | | | 40 | super(record ?? EMPTY_DAG_RECORD); |
| b69ab31 | | | 41 | } |
| b69ab31 | | | 42 | |
| b69ab31 | | | 43 | static fromDag(commitDag?: BaseDag<DagCommitInfo>, mutationDag?: MutationDag): Dag { |
| b69ab31 | | | 44 | return new Dag(CommitDagRecord({commitDag, mutationDag})); |
| b69ab31 | | | 45 | } |
| b69ab31 | | | 46 | |
| b69ab31 | | | 47 | // Delegates |
| b69ab31 | | | 48 | |
| b69ab31 | | | 49 | get commitDag(): BaseDag<DagCommitInfo> { |
| b69ab31 | | | 50 | return this.inner.commitDag; |
| b69ab31 | | | 51 | } |
| b69ab31 | | | 52 | |
| b69ab31 | | | 53 | get mutationDag(): MutationDag { |
| b69ab31 | | | 54 | return this.inner.mutationDag; |
| b69ab31 | | | 55 | } |
| b69ab31 | | | 56 | |
| b69ab31 | | | 57 | private withCommitDag(f: (dag: BaseDag<DagCommitInfo>) => BaseDag<DagCommitInfo>): Dag { |
| b69ab31 | | | 58 | const newCommitDag = f(this.commitDag); |
| b69ab31 | | | 59 | const newRecord = this.inner.set('commitDag', newCommitDag); |
| b69ab31 | | | 60 | return new Dag(newRecord); |
| b69ab31 | | | 61 | } |
| b69ab31 | | | 62 | |
| b69ab31 | | | 63 | // Basic edit |
| b69ab31 | | | 64 | |
| b69ab31 | | | 65 | add(commits: Iterable<DagCommitInfo>): Dag { |
| b69ab31 | | | 66 | // When adding commits, also update the mutationDag. |
| b69ab31 | | | 67 | const seqNumber = this.inner.nextSeqNumber; |
| b69ab31 | | | 68 | const commitArray = [...commits].map(c => |
| b69ab31 | | | 69 | c |
| b69ab31 | | | 70 | // Assign `seqNumber` (insertion order) to help sorting commits later. |
| b69ab31 | | | 71 | // The seqNumber is the same for all `commits` so the order does not matter. |
| b69ab31 | | | 72 | .set('seqNumber', c.seqNumber ?? seqNumber) |
| b69ab31 | | | 73 | // Extend `parents` for dagwalkerForRendering to properly connect public commits |
| b69ab31 | | | 74 | .set('parents', [...c.parents, ...c.grandparents]) |
| b69ab31 | | | 75 | // Assign `ancestors` for dagWalkerForRendering to connect public commits properly |
| b69ab31 | | | 76 | .set('ancestors', c.grandparents.length > 0 ? List(c.grandparents) : c.ancestors), |
| b69ab31 | | | 77 | ); |
| b69ab31 | | | 78 | const oldNewPairs = new Array<[Hash, Hash]>(); |
| b69ab31 | | | 79 | for (const info of commitArray) { |
| b69ab31 | | | 80 | info.closestPredecessors?.forEach(p => oldNewPairs.push([p, info.hash])); |
| b69ab31 | | | 81 | if (info.successorInfo != null) { |
| b69ab31 | | | 82 | oldNewPairs.push([info.hash, info.successorInfo.hash]); |
| b69ab31 | | | 83 | } |
| b69ab31 | | | 84 | } |
| b69ab31 | | | 85 | |
| b69ab31 | | | 86 | // Update nameMap. |
| b69ab31 | | | 87 | const toDelete = commitArray.map(c => this.get(c.hash)).filter(notEmpty); |
| b69ab31 | | | 88 | const nameMap = calculateNewNameMap(this.inner.nameMap, toDelete, commitArray); |
| b69ab31 | | | 89 | |
| b69ab31 | | | 90 | // Update other fields. |
| b69ab31 | | | 91 | const commitDag = this.commitDag.add(commitArray); |
| b69ab31 | | | 92 | const mutationDag = this.mutationDag.addMutations(oldNewPairs); |
| b69ab31 | | | 93 | const nextSeqNumber = seqNumber + 1; |
| b69ab31 | | | 94 | const record = this.inner.merge({ |
| b69ab31 | | | 95 | commitDag, |
| b69ab31 | | | 96 | mutationDag, |
| b69ab31 | | | 97 | nameMap, |
| b69ab31 | | | 98 | nextSeqNumber, |
| b69ab31 | | | 99 | }); |
| b69ab31 | | | 100 | return new Dag(record); |
| b69ab31 | | | 101 | } |
| b69ab31 | | | 102 | |
| b69ab31 | | | 103 | /** See MutationDag.addMutations. */ |
| b69ab31 | | | 104 | addMutations(oldNewPairs: Iterable<[Hash, Hash]>): Dag { |
| b69ab31 | | | 105 | const newMutationDag = this.mutationDag.addMutations(oldNewPairs); |
| b69ab31 | | | 106 | const newRecord = this.inner.set('mutationDag', newMutationDag); |
| b69ab31 | | | 107 | return new Dag(newRecord); |
| b69ab31 | | | 108 | } |
| b69ab31 | | | 109 | |
| b69ab31 | | | 110 | remove(set: SetLike): Dag { |
| b69ab31 | | | 111 | // When removing commits, don't remove them from the mutationDag intentionally. |
| b69ab31 | | | 112 | const hashSet = HashSet.fromHashes(set); |
| b69ab31 | | | 113 | const toDelete = this.getBatch(hashSet.toArray()); |
| b69ab31 | | | 114 | const nameMap = calculateNewNameMap(this.inner.nameMap, toDelete, []); |
| b69ab31 | | | 115 | const commitDag = this.commitDag.remove(hashSet); |
| b69ab31 | | | 116 | const record = this.inner.merge({ |
| b69ab31 | | | 117 | commitDag, |
| b69ab31 | | | 118 | nameMap, |
| b69ab31 | | | 119 | }); |
| b69ab31 | | | 120 | return new Dag(record); |
| b69ab31 | | | 121 | } |
| b69ab31 | | | 122 | |
| b69ab31 | | | 123 | /** A callback form of remove() and add(). */ |
| b69ab31 | | | 124 | replaceWith( |
| b69ab31 | | | 125 | set: SetLike, |
| b69ab31 | | | 126 | replaceFunc: (h: Hash, c?: DagCommitInfo) => DagCommitInfo | undefined, |
| b69ab31 | | | 127 | ): Dag { |
| b69ab31 | | | 128 | const hashSet = HashSet.fromHashes(set); |
| b69ab31 | | | 129 | const hashes = hashSet.toHashes(); |
| b69ab31 | | | 130 | return this.remove(this.present(set)).add( |
| b69ab31 | | | 131 | hashes |
| b69ab31 | | | 132 | .map(h => replaceFunc(h, this.get(h))) |
| b69ab31 | | | 133 | .filter(c => c != undefined) as Iterable<DagCommitInfo>, |
| b69ab31 | | | 134 | ); |
| b69ab31 | | | 135 | } |
| b69ab31 | | | 136 | |
| b69ab31 | | | 137 | // Basic query |
| b69ab31 | | | 138 | |
| b69ab31 | | | 139 | get(hash: Hash | undefined | null): DagCommitInfo | undefined { |
| b69ab31 | | | 140 | return this.commitDag.get(hash); |
| b69ab31 | | | 141 | } |
| b69ab31 | | | 142 | |
| b69ab31 | | | 143 | getBatch(hashes: Array<Hash>): Array<DagCommitInfo> { |
| b69ab31 | | | 144 | return hashes.map(h => this.get(h)).filter(notEmpty); |
| b69ab31 | | | 145 | } |
| b69ab31 | | | 146 | |
| b69ab31 | | | 147 | has(hash: Hash | undefined | null): boolean { |
| b69ab31 | | | 148 | return this.commitDag.has(hash); |
| b69ab31 | | | 149 | } |
| b69ab31 | | | 150 | |
| b69ab31 | | | 151 | [Symbol.iterator](): IterableIterator<Hash> { |
| b69ab31 | | | 152 | return this.commitDag[Symbol.iterator](); |
| b69ab31 | | | 153 | } |
| b69ab31 | | | 154 | |
| b69ab31 | | | 155 | values(): Iterable<Readonly<DagCommitInfo>> { |
| b69ab31 | | | 156 | return this.commitDag.values(); |
| b69ab31 | | | 157 | } |
| b69ab31 | | | 158 | |
| b69ab31 | | | 159 | parentHashes(hash: Hash): Readonly<Hash[]> { |
| b69ab31 | | | 160 | return this.commitDag.parentHashes(hash); |
| b69ab31 | | | 161 | } |
| b69ab31 | | | 162 | |
| b69ab31 | | | 163 | childHashes(hash: Hash): List<Hash> { |
| b69ab31 | | | 164 | return this.commitDag.childHashes(hash); |
| b69ab31 | | | 165 | } |
| b69ab31 | | | 166 | |
| b69ab31 | | | 167 | // High-level query |
| b69ab31 | | | 168 | |
| b69ab31 | | | 169 | /** Return hashes present in this dag. */ |
| b69ab31 | | | 170 | present(set: SetLike): HashSet { |
| b69ab31 | | | 171 | return this.commitDag.present(set); |
| b69ab31 | | | 172 | } |
| b69ab31 | | | 173 | |
| b69ab31 | | | 174 | parents(set: SetLike): HashSet { |
| b69ab31 | | | 175 | return this.commitDag.parents(set); |
| b69ab31 | | | 176 | } |
| b69ab31 | | | 177 | |
| b69ab31 | | | 178 | children(set: SetLike): HashSet { |
| b69ab31 | | | 179 | return this.commitDag.children(set); |
| b69ab31 | | | 180 | } |
| b69ab31 | | | 181 | |
| b69ab31 | | | 182 | ancestors(set: SetLike, props?: {within?: SetLike}): HashSet { |
| b69ab31 | | | 183 | return this.commitDag.ancestors(set, props); |
| b69ab31 | | | 184 | } |
| b69ab31 | | | 185 | |
| b69ab31 | | | 186 | descendants(set: SetLike, props?: {within?: SetLike}): HashSet { |
| b69ab31 | | | 187 | return this.commitDag.descendants(set, props); |
| b69ab31 | | | 188 | } |
| b69ab31 | | | 189 | |
| b69ab31 | | | 190 | range(roots: SetLike, heads: SetLike): HashSet { |
| b69ab31 | | | 191 | return this.commitDag.range(roots, heads); |
| b69ab31 | | | 192 | } |
| b69ab31 | | | 193 | |
| b69ab31 | | | 194 | roots = cachedMethod(this.rootsImpl, {cache: rootsCache}); |
| b69ab31 | | | 195 | private rootsImpl(set: SetLike): HashSet { |
| b69ab31 | | | 196 | return this.commitDag.roots(set); |
| b69ab31 | | | 197 | } |
| b69ab31 | | | 198 | |
| b69ab31 | | | 199 | heads = cachedMethod(this.headsImpl, {cache: headsCache}); |
| b69ab31 | | | 200 | private headsImpl(set: SetLike): HashSet { |
| b69ab31 | | | 201 | return this.commitDag.heads(set); |
| b69ab31 | | | 202 | } |
| b69ab31 | | | 203 | |
| b69ab31 | | | 204 | gca(set1: SetLike, set2: SetLike): HashSet { |
| b69ab31 | | | 205 | return this.commitDag.gca(set1, set2); |
| b69ab31 | | | 206 | } |
| b69ab31 | | | 207 | |
| b69ab31 | | | 208 | isAncestor(ancestor: Hash, descendant: Hash): boolean { |
| b69ab31 | | | 209 | return this.commitDag.isAncestor(ancestor, descendant); |
| b69ab31 | | | 210 | } |
| b69ab31 | | | 211 | |
| b69ab31 | | | 212 | filter(predicate: (commit: Readonly<DagCommitInfo>) => boolean, set?: SetLike): HashSet { |
| b69ab31 | | | 213 | return this.commitDag.filter(predicate, set); |
| b69ab31 | | | 214 | } |
| b69ab31 | | | 215 | |
| b69ab31 | | | 216 | all = cachedMethod(this.allImpl, {cache: allCache}); |
| b69ab31 | | | 217 | private allImpl(): HashSet { |
| b69ab31 | | | 218 | return HashSet.fromHashes(this); |
| b69ab31 | | | 219 | } |
| b69ab31 | | | 220 | |
| b69ab31 | | | 221 | /** |
| b69ab31 | | | 222 | * Return a subset suitable for rendering. This filters out: |
| b69ab31 | | | 223 | * - Obsoleted stack. Only roots(obsolete()), heads(obsolete()), and |
| b69ab31 | | | 224 | * parents(draft()) are kept. |
| b69ab31 | | | 225 | * - Unnamed public commits that do not have direct draft children. |
| b69ab31 | | | 226 | */ |
| b69ab31 | | | 227 | subsetForRendering = cachedMethod(this.subsetForRenderingImpl, {cache: subsetForRenderingCache}); |
| b69ab31 | | | 228 | private subsetForRenderingImpl(set?: SetLike, condenseObsoleteStacks: boolean = true): HashSet { |
| b69ab31 | | | 229 | const all = set === undefined ? this.all() : HashSet.fromHashes(set); |
| b69ab31 | | | 230 | const draft = this.draft(all); |
| b69ab31 | | | 231 | const unnamedPublic = this.filter( |
| b69ab31 | | | 232 | i => |
| b69ab31 | | | 233 | i.phase === 'public' && |
| b69ab31 | | | 234 | i.remoteBookmarks.length === 0 && |
| b69ab31 | | | 235 | i.bookmarks.length === 0 && |
| b69ab31 | | | 236 | (i.stableCommitMetadata == null || i.stableCommitMetadata.length === 0) && |
| b69ab31 | | | 237 | !i.isDot, |
| b69ab31 | | | 238 | all, |
| b69ab31 | | | 239 | ); |
| ab83ad3 | | | 240 | const dot = this.resolve('.'); |
| ab83ad3 | | | 241 | const dotAncestors = dot != null ? this.ancestors(HashSet.fromHashes([dot.hash])) : HashSet.fromHashes([]); |
| ab83ad3 | | | 242 | const toHidePublic = unnamedPublic.subtract(this.parents(draft)).subtract(dotAncestors); |
| b69ab31 | | | 243 | let toHide = toHidePublic; |
| b69ab31 | | | 244 | if (condenseObsoleteStacks) { |
| b69ab31 | | | 245 | const obsolete = this.obsolete(all); |
| b69ab31 | | | 246 | const toKeep = this.parents(draft.subtract(obsolete)) |
| b69ab31 | | | 247 | .union(this.roots(obsolete)) |
| b69ab31 | | | 248 | .union(this.heads(obsolete)); |
| b69ab31 | | | 249 | toHide = obsolete.subtract(toKeep).union(toHidePublic); |
| b69ab31 | | | 250 | } |
| b69ab31 | | | 251 | return all.subtract(toHide); |
| b69ab31 | | | 252 | } |
| b69ab31 | | | 253 | |
| b69ab31 | | | 254 | // Sort |
| b69ab31 | | | 255 | |
| b69ab31 | | | 256 | /** |
| b69ab31 | | | 257 | * sortAsc all commits, with the default compare function. |
| b69ab31 | | | 258 | * Return `[map, array]`. The array is the sorted hashes. |
| b69ab31 | | | 259 | * The map provides look-up from hash to array index. `map.get(h)` is `array.indexOf(h)`. |
| b69ab31 | | | 260 | */ |
| b69ab31 | | | 261 | defaultSortAscIndex = cachedMethod(this.defaultSortAscIndexImpl, { |
| b69ab31 | | | 262 | cache: defaultSortAscIndexCache, |
| b69ab31 | | | 263 | }); |
| b69ab31 | | | 264 | private defaultSortAscIndexImpl(): [ReadonlyMap<Hash, number>, ReadonlyArray<Hash>] { |
| b69ab31 | | | 265 | const sorted = this.commitDag.sortAsc(this.all(), {compare: sortAscCompare, gap: false}); |
| b69ab31 | | | 266 | const index = new Map(sorted.map((h, i) => [h, i])); |
| b69ab31 | | | 267 | return [index, sorted]; |
| b69ab31 | | | 268 | } |
| b69ab31 | | | 269 | |
| b69ab31 | | | 270 | sortAsc(set: SetLike, props?: SortProps<DagCommitInfo>): Array<Hash> { |
| b69ab31 | | | 271 | if (props?.compare == null) { |
| b69ab31 | | | 272 | // If no custom compare function, use the sortAsc index to answer subset |
| b69ab31 | | | 273 | // sortAsc, which can be slow to calculate otherwise. |
| b69ab31 | | | 274 | const [index, sorted] = this.defaultSortAscIndex(); |
| b69ab31 | | | 275 | if (set === undefined) { |
| b69ab31 | | | 276 | return [...sorted]; |
| b69ab31 | | | 277 | } |
| b69ab31 | | | 278 | const hashes = arrayFromHashes(set === undefined ? this.all() : set); |
| b69ab31 | | | 279 | return hashes.sort((a, b) => { |
| b69ab31 | | | 280 | const aIdx = index.get(a); |
| b69ab31 | | | 281 | const bIdx = index.get(b); |
| b69ab31 | | | 282 | if (aIdx == null || bIdx == null) { |
| b69ab31 | | | 283 | throw new Error(`Commit ${a} or ${b} is not in the dag.`); |
| b69ab31 | | | 284 | } |
| b69ab31 | | | 285 | return aIdx - bIdx; |
| b69ab31 | | | 286 | }); |
| b69ab31 | | | 287 | } |
| b69ab31 | | | 288 | // Otherwise, fallback to sortAsc. |
| b69ab31 | | | 289 | return this.commitDag.sortAsc(set, {compare: sortAscCompare, ...props}); |
| b69ab31 | | | 290 | } |
| b69ab31 | | | 291 | |
| b69ab31 | | | 292 | sortDesc(set: SetLike, props?: SortProps<DagCommitInfo>): Array<Hash> { |
| b69ab31 | | | 293 | return this.commitDag.sortDesc(set, props); |
| b69ab31 | | | 294 | } |
| b69ab31 | | | 295 | |
| b69ab31 | | | 296 | // Filters |
| b69ab31 | | | 297 | |
| b69ab31 | | | 298 | obsolete(set?: SetLike): HashSet { |
| b69ab31 | | | 299 | return this.filter(c => c.successorInfo != null, set); |
| b69ab31 | | | 300 | } |
| b69ab31 | | | 301 | |
| b69ab31 | | | 302 | nonObsolete(set?: SetLike): HashSet { |
| b69ab31 | | | 303 | return this.filter(c => c.successorInfo == null, set); |
| b69ab31 | | | 304 | } |
| b69ab31 | | | 305 | |
| b69ab31 | | | 306 | public_(set?: SetLike): HashSet { |
| b69ab31 | | | 307 | return this.filter(c => c.phase === 'public', set); |
| b69ab31 | | | 308 | } |
| b69ab31 | | | 309 | |
| b69ab31 | | | 310 | draft(set?: SetLike): HashSet { |
| b69ab31 | | | 311 | return this.filter(c => (c.phase ?? 'draft') === 'draft', set); |
| b69ab31 | | | 312 | } |
| b69ab31 | | | 313 | |
| b69ab31 | | | 314 | merge(set?: SetLike): HashSet { |
| b69ab31 | | | 315 | return this.commitDag.merge(set); |
| b69ab31 | | | 316 | } |
| b69ab31 | | | 317 | |
| b69ab31 | | | 318 | // Edit APIs that are less generic, require `C` to be `CommitInfo`. |
| b69ab31 | | | 319 | |
| b69ab31 | | | 320 | /** Bump the timestamp of descendants(set) to "now". */ |
| b69ab31 | | | 321 | touch(set: SetLike, includeDescendants = true): Dag { |
| b69ab31 | | | 322 | const affected = includeDescendants ? this.descendants(set) : set; |
| b69ab31 | | | 323 | const now = new Date(); |
| b69ab31 | | | 324 | return this.replaceWith(affected, (_h, c) => c?.set('date', now)); |
| b69ab31 | | | 325 | } |
| b69ab31 | | | 326 | |
| b69ab31 | | | 327 | /** |
| b69ab31 | | | 328 | * Remove obsoleted commits that no longer have non-obsoleted descendants. |
| b69ab31 | | | 329 | * If `startHeads` is not set, scan all obsoleted draft heads. Otherwise, |
| b69ab31 | | | 330 | * limit the scan to the given heads. |
| b69ab31 | | | 331 | */ |
| b69ab31 | | | 332 | cleanup(startHeads?: SetLike): Dag { |
| b69ab31 | | | 333 | // ancestors(".") are not obsoleted. |
| b69ab31 | | | 334 | const obsolete = this.obsolete().subtract(this.ancestors(this.resolve('.')?.hash)); |
| b69ab31 | | | 335 | // Don't trust `startHeads` as obsoleted draft heads, so we calculate it anyway. |
| b69ab31 | | | 336 | let heads = this.heads(this.draft()).intersect(obsolete); |
| b69ab31 | | | 337 | if (startHeads !== undefined) { |
| b69ab31 | | | 338 | heads = heads.intersect(HashSet.fromHashes(startHeads)); |
| b69ab31 | | | 339 | } |
| b69ab31 | | | 340 | const toRemove = this.ancestors(heads, {within: obsolete}); |
| b69ab31 | | | 341 | return this.remove(toRemove); |
| b69ab31 | | | 342 | } |
| b69ab31 | | | 343 | |
| b69ab31 | | | 344 | /** |
| b69ab31 | | | 345 | * Attempt to rebase `srcSet` to `dest` for preview use-case. |
| b69ab31 | | | 346 | * Handles case that produces "orphaned" or "obsoleted" commits. |
| b69ab31 | | | 347 | * Does not handle: |
| b69ab31 | | | 348 | * - copy 'x amended to y' relation when x and y are both being rebased. |
| b69ab31 | | | 349 | * - skip rebasing 'x' if 'x amended to y' and 'y in ancestors(dest)'. |
| b69ab31 | | | 350 | */ |
| b69ab31 | | | 351 | rebase(srcSet: SetLike, dest: Hash | undefined): Dag { |
| b69ab31 | | | 352 | let src = HashSet.fromHashes(srcSet); |
| b69ab31 | | | 353 | // x is already rebased, if x's parent is dest or 'already rebased'. |
| b69ab31 | | | 354 | // dest--a--b--c--d--e: when rebasing a+b+d+e to dest, only a+b are already rebased. |
| b69ab31 | | | 355 | const alreadyRebased = this.descendants(dest, {within: src}); |
| b69ab31 | | | 356 | // Skip already rebased, and skip non-draft commits. |
| b69ab31 | | | 357 | src = this.draft(src.subtract(alreadyRebased)); |
| b69ab31 | | | 358 | // Nothing to rebase? |
| b69ab31 | | | 359 | if (dest == null || src.size === 0) { |
| b69ab31 | | | 360 | return this; |
| b69ab31 | | | 361 | } |
| b69ab31 | | | 362 | // Rebase is not simply moving `roots(src)` to `dest`. Consider graph 'a--b--c--d', |
| b69ab31 | | | 363 | // 'rebase -r a+b+d -d dest' produces 'dest--a--b--d' and 'a(obsoleted)--b(obsoleted)--c': |
| b69ab31 | | | 364 | // - The new parent of 'd' is 'b', not 'dest'. |
| b69ab31 | | | 365 | // - 'a' and 'b' got duplicated. |
| b69ab31 | | | 366 | const srcRoots = this.roots(src); // a, d |
| b69ab31 | | | 367 | const orphaned = this.range(src, this.draft()).subtract(src); // c |
| b69ab31 | | | 368 | const duplicated = this.ancestors(orphaned).intersect(src); // a, b |
| b69ab31 | | | 369 | const maybeSuccHash = (h: Hash) => (duplicated.contains(h) ? `${REBASE_SUCC_PREFIX}${h}` : h); |
| b69ab31 | | | 370 | const date = new Date(); |
| b69ab31 | | | 371 | const newParents = (h: Hash): Hash[] => { |
| b69ab31 | | | 372 | const directParents = this.parents(h); |
| b69ab31 | | | 373 | let parents = directParents.intersect(src); |
| b69ab31 | | | 374 | if (parents.size === 0) { |
| b69ab31 | | | 375 | parents = this.heads(this.ancestors(directParents).intersect(src)); |
| b69ab31 | | | 376 | } |
| b69ab31 | | | 377 | return parents.size === 0 ? [dest] : parents.toHashes().map(maybeSuccHash).toArray(); |
| b69ab31 | | | 378 | }; |
| b69ab31 | | | 379 | const toCleanup = this.parents(srcRoots); |
| b69ab31 | | | 380 | return this.replaceWith(src.union(duplicated.toHashes().map(maybeSuccHash)), (h, c) => { |
| b69ab31 | | | 381 | const isSucc = h.startsWith(REBASE_SUCC_PREFIX); |
| b69ab31 | | | 382 | const pureHash = isSucc ? h.substring(REBASE_SUCC_PREFIX.length) : h; |
| b69ab31 | | | 383 | const isPred = !isSucc && duplicated.contains(h); |
| b69ab31 | | | 384 | const isRoot = srcRoots.contains(pureHash); |
| b69ab31 | | | 385 | const info = nullthrows(isSucc ? this.get(pureHash) : c); |
| b69ab31 | | | 386 | return info.withMutations(mut => { |
| b69ab31 | | | 387 | // Reset the seqNumber so the rebase preview tends to show as right-most branches. |
| b69ab31 | | | 388 | let newInfo = mut.set('seqNumber', undefined); |
| b69ab31 | | | 389 | if (isPred) { |
| b69ab31 | | | 390 | // For "predecessors" (ex. a(obsoleted)), keep hash unchanged |
| b69ab31 | | | 391 | // so orphaned commits (c) don't move. Update successorInfo. |
| b69ab31 | | | 392 | const succHash = maybeSuccHash(pureHash); |
| b69ab31 | | | 393 | newInfo = newInfo.set('successorInfo', {hash: succHash, type: 'rebase'}); |
| b69ab31 | | | 394 | } else { |
| b69ab31 | | | 395 | // Set date, parents, previewType. |
| b69ab31 | | | 396 | newInfo = newInfo.merge({ |
| b69ab31 | | | 397 | date, |
| b69ab31 | | | 398 | parents: newParents(pureHash), |
| b69ab31 | | | 399 | previewType: isRoot |
| b69ab31 | | | 400 | ? CommitPreview.REBASE_OPTIMISTIC_ROOT |
| b69ab31 | | | 401 | : CommitPreview.REBASE_OPTIMISTIC_DESCENDANT, |
| b69ab31 | | | 402 | }); |
| b69ab31 | | | 403 | // Set predecessor info for successors. |
| b69ab31 | | | 404 | if (isSucc) { |
| b69ab31 | | | 405 | newInfo = newInfo.merge({ |
| b69ab31 | | | 406 | closestPredecessors: [pureHash], |
| b69ab31 | | | 407 | hash: h, |
| b69ab31 | | | 408 | }); |
| b69ab31 | | | 409 | } |
| b69ab31 | | | 410 | } |
| b69ab31 | | | 411 | return newInfo; |
| b69ab31 | | | 412 | }); |
| b69ab31 | | | 413 | }).cleanup(toCleanup); |
| b69ab31 | | | 414 | } |
| b69ab31 | | | 415 | |
| b69ab31 | | | 416 | /** |
| b69ab31 | | | 417 | * Force the disconnected public commits to be connected to each other |
| b69ab31 | | | 418 | * in chronological order. |
| b69ab31 | | | 419 | * |
| b69ab31 | | | 420 | * This is "incorrect" but we don't get the info from `sl log` yet. |
| b69ab31 | | | 421 | * |
| b69ab31 | | | 422 | * Useful to reason about ancestry relations. For example, to filter |
| b69ab31 | | | 423 | * out rebase destinations (ex. remote/stable) that are backwards, |
| b69ab31 | | | 424 | * we want ancestors(rebase_src) to include public commits like |
| b69ab31 | | | 425 | * remote/stable. |
| b69ab31 | | | 426 | */ |
| b69ab31 | | | 427 | private forceConnectPublic(): Dag { |
| b69ab31 | | | 428 | // Not all public commits need this "fix". Only consider the "roots". |
| b69ab31 | | | 429 | const toFix = this.roots(this.public_()); |
| b69ab31 | | | 430 | const sorted = toFix |
| b69ab31 | | | 431 | .toList() |
| b69ab31 | | | 432 | .sortBy(h => this.get(h)?.date.valueOf() ?? 0) |
| b69ab31 | | | 433 | .toArray(); |
| b69ab31 | | | 434 | const parentPairs: Array<[Hash, Hash]> = sorted.flatMap((h, i) => |
| b69ab31 | | | 435 | i === 0 ? [] : [[h, sorted[i - 1]]], |
| b69ab31 | | | 436 | ); |
| b69ab31 | | | 437 | const parentMap = new Map<Hash, Hash>(parentPairs); |
| b69ab31 | | | 438 | return this.replaceWith(toFix, (h, c) => { |
| b69ab31 | | | 439 | const newParent = parentMap.get(h); |
| b69ab31 | | | 440 | if (c == null || newParent == null) { |
| b69ab31 | | | 441 | return c; |
| b69ab31 | | | 442 | } |
| b69ab31 | | | 443 | return c.withMutations(m => |
| b69ab31 | | | 444 | m.set('parents', [...c.parents, newParent]).set('ancestors', List([newParent])), |
| b69ab31 | | | 445 | ); |
| b69ab31 | | | 446 | }); |
| b69ab31 | | | 447 | } |
| b69ab31 | | | 448 | |
| b69ab31 | | | 449 | /** |
| b69ab31 | | | 450 | * A backward compatible solution to connect public commits |
| b69ab31 | | | 451 | * using grandparents info from sapling. |
| b69ab31 | | | 452 | * |
| b69ab31 | | | 453 | * If an older version of sapling that doesn't support "grandparents" |
| b69ab31 | | | 454 | * is running, all the grandparents fields will be empty. Fallback |
| b69ab31 | | | 455 | * to chronological connections (forceConnectPublic) in this case. |
| b69ab31 | | | 456 | * |
| b69ab31 | | | 457 | * The function needs to be manually called to connect the nodes, |
| b69ab31 | | | 458 | * in order to work with forceConnectPublic() to ensure backward compatibility. |
| b69ab31 | | | 459 | * After the sapling patch gets fully released, we should consider consuming |
| b69ab31 | | | 460 | * "grandparents" naturally, without having to explicitly update other commit fields. |
| b69ab31 | | | 461 | */ |
| b69ab31 | | | 462 | maybeForceConnectPublic(): Dag { |
| b69ab31 | | | 463 | const toConnect = this.roots(this.public_()); |
| b69ab31 | | | 464 | for (const h of toConnect) { |
| b69ab31 | | | 465 | const c = this.get(h); |
| b69ab31 | | | 466 | if (c != null && c.grandparents.length > 0) { |
| b69ab31 | | | 467 | return this; |
| b69ab31 | | | 468 | } |
| b69ab31 | | | 469 | } |
| b69ab31 | | | 470 | return this.forceConnectPublic(); |
| b69ab31 | | | 471 | } |
| b69ab31 | | | 472 | |
| b69ab31 | | | 473 | // Query APIs that are less generic, require `C` to be `CommitInfo`. |
| b69ab31 | | | 474 | |
| b69ab31 | | | 475 | /** All visible successors recursively, including `set`. */ |
| b69ab31 | | | 476 | successors(set: SetLike): HashSet { |
| b69ab31 | | | 477 | return this.mutationDag.range(set, this); |
| b69ab31 | | | 478 | } |
| b69ab31 | | | 479 | |
| b69ab31 | | | 480 | /** All visible predecessors of commits in `set`, including `set`. */ |
| b69ab31 | | | 481 | predecessors(set: SetLike): HashSet { |
| b69ab31 | | | 482 | return this.present(this.mutationDag.ancestors(set)); |
| b69ab31 | | | 483 | } |
| b69ab31 | | | 484 | |
| b69ab31 | | | 485 | /** |
| b69ab31 | | | 486 | * Follow successors for the given set. |
| b69ab31 | | | 487 | * |
| b69ab31 | | | 488 | * - If a hash does not have successors in this `dag`, then this hash |
| b69ab31 | | | 489 | * will be included in the result. |
| b69ab31 | | | 490 | * - If a hash has multiple successors, only the "head" successor that |
| b69ab31 | | | 491 | * is also in this `dag` will be returned, the hash itself will be |
| b69ab31 | | | 492 | * excluded from the result. |
| b69ab31 | | | 493 | * - If `set` contains a hash that gets split into multiple successors |
| b69ab31 | | | 494 | * that heads(successors) on the mutation graph still contains multiple |
| b69ab31 | | | 495 | * commits, then heads(ancestors(successors)) on the main graph will |
| b69ab31 | | | 496 | * be attempted to pick the "stack top". |
| b69ab31 | | | 497 | * |
| b69ab31 | | | 498 | * For example, consider the successor relations: |
| b69ab31 | | | 499 | * |
| b69ab31 | | | 500 | * A-->A1-->A2-->A3 |
| b69ab31 | | | 501 | * |
| b69ab31 | | | 502 | * and if the current graph only has 'A1', 'A2' and 'B'. |
| b69ab31 | | | 503 | * followSuccessors(['A', 'B']) will return ['A2', 'B']. |
| b69ab31 | | | 504 | * successors(['A', 'B']) will return ['A', 'A1', 'A2', 'B']. |
| b69ab31 | | | 505 | */ |
| b69ab31 | | | 506 | followSuccessors(set: SetLike): HashSet { |
| b69ab31 | | | 507 | const hashSet = HashSet.fromHashes(set); |
| b69ab31 | | | 508 | const mDag = this.mutationDag; |
| b69ab31 | | | 509 | let successors = mDag.heads(mDag.range(hashSet, this)); |
| b69ab31 | | | 510 | // When following a split to multiple successors, consider using |
| b69ab31 | | | 511 | // the main dag to pick the stack top. |
| b69ab31 | | | 512 | if (hashSet.size === 1 && successors.size > 1) { |
| b69ab31 | | | 513 | successors = this.heads(this.ancestors(successors)); |
| b69ab31 | | | 514 | } |
| b69ab31 | | | 515 | const obsoleted = mDag.ancestors(mDag.parents(successors)); |
| b69ab31 | | | 516 | return hashSet.subtract(obsoleted).union(successors); |
| b69ab31 | | | 517 | } |
| b69ab31 | | | 518 | |
| b69ab31 | | | 519 | /** Attempt to resolve a name by `name`. The `name` can be a hash, a bookmark name, etc. */ |
| b69ab31 | | | 520 | resolve(name: string): Readonly<DagCommitInfo> | undefined { |
| b69ab31 | | | 521 | // See `hg help revision` and context.py (changectx.__init__), |
| b69ab31 | | | 522 | // namespaces.py for priorities. Basically (in this order): |
| b69ab31 | | | 523 | // - hex full hash (40 bytes); '.' (working parent) |
| b69ab31 | | | 524 | // - nameMap (see infoToNameMapEntries) |
| b69ab31 | | | 525 | // - partial match (unambiguous partial prefix match) |
| b69ab31 | | | 526 | |
| b69ab31 | | | 527 | // Full commit hash? |
| b69ab31 | | | 528 | const info = this.get(name); |
| b69ab31 | | | 529 | if (info) { |
| b69ab31 | | | 530 | return info; |
| b69ab31 | | | 531 | } |
| b69ab31 | | | 532 | |
| b69ab31 | | | 533 | // Namemap lookup. |
| b69ab31 | | | 534 | const entries = this.inner.nameMap.get(name); |
| b69ab31 | | | 535 | if (entries) { |
| b69ab31 | | | 536 | let best: HashPriRecord | null = null; |
| b69ab31 | | | 537 | for (const entry of entries) { |
| b69ab31 | | | 538 | if (best == null || best.priority > entry.priority) { |
| b69ab31 | | | 539 | best = entry; |
| b69ab31 | | | 540 | } |
| b69ab31 | | | 541 | } |
| b69ab31 | | | 542 | if (best != null) { |
| b69ab31 | | | 543 | return this.get(best.hash); |
| b69ab31 | | | 544 | } |
| b69ab31 | | | 545 | } |
| b69ab31 | | | 546 | |
| b69ab31 | | | 547 | // Unambiguous prefix match. |
| b69ab31 | | | 548 | if (shouldPrefixMatch(name)) { |
| b69ab31 | | | 549 | let matched: undefined | Hash = undefined; |
| b69ab31 | | | 550 | for (const hash of this) { |
| b69ab31 | | | 551 | if (hash.startsWith(name)) { |
| b69ab31 | | | 552 | if (matched === undefined) { |
| b69ab31 | | | 553 | matched = hash; |
| b69ab31 | | | 554 | } else { |
| b69ab31 | | | 555 | // Ambiguous prefix. |
| b69ab31 | | | 556 | return undefined; |
| b69ab31 | | | 557 | } |
| b69ab31 | | | 558 | } |
| b69ab31 | | | 559 | } |
| b69ab31 | | | 560 | return matched !== undefined ? this.get(matched) : undefined; |
| b69ab31 | | | 561 | } |
| b69ab31 | | | 562 | |
| b69ab31 | | | 563 | // No match. |
| b69ab31 | | | 564 | return undefined; |
| b69ab31 | | | 565 | } |
| b69ab31 | | | 566 | |
| b69ab31 | | | 567 | /** Render `set` into `ExtendedGraphRow`s. */ |
| b69ab31 | | | 568 | renderToRows = cachedMethod(this.renderToRowsImpl, {cache: renderToRowsCache}); |
| b69ab31 | | | 569 | private renderToRowsImpl(set?: SetLike): ReadonlyArray<[DagCommitInfo, ExtendedGraphRow]> { |
| b69ab31 | | | 570 | const renderer = new Renderer(); |
| b69ab31 | | | 571 | const rows: Array<[DagCommitInfo, ExtendedGraphRow]> = []; |
| b69ab31 | | | 572 | for (const [type, item] of this.dagWalkerForRendering(set)) { |
| b69ab31 | | | 573 | if (type === 'row') { |
| b69ab31 | | | 574 | const [info, typedParents] = item; |
| b69ab31 | | | 575 | const forceLastColumn = info.isYouAreHere; |
| b69ab31 | | | 576 | const row = renderer.nextRow(info.hash, typedParents, {forceLastColumn}); |
| b69ab31 | | | 577 | rows.push([info, row]); |
| b69ab31 | | | 578 | } else if (type === 'reserve') { |
| b69ab31 | | | 579 | renderer.reserve(item); |
| b69ab31 | | | 580 | } |
| b69ab31 | | | 581 | } |
| b69ab31 | | | 582 | return rows; |
| b69ab31 | | | 583 | } |
| b69ab31 | | | 584 | |
| b69ab31 | | | 585 | /** |
| b69ab31 | | | 586 | * Yield [Info, Ancestor[]] in order, to be used by the rendering logic. |
| b69ab31 | | | 587 | * This returns a generator. To walk through the entire DAG it can be slow. |
| b69ab31 | | | 588 | * Use `dagWalkerForRendering` if you want a cached version. |
| b69ab31 | | | 589 | */ |
| b69ab31 | | | 590 | *dagWalkerForRendering( |
| b69ab31 | | | 591 | set?: SetLike, |
| b69ab31 | | | 592 | ): Iterable<['reserve', Hash] | ['row', [DagCommitInfo, Ancestor[]]]> { |
| b69ab31 | | | 593 | // We want sortDesc, but want to reuse the comprehensive sortAsc compare logic. |
| b69ab31 | | | 594 | // So we use sortAsc here, then reverse it. |
| b69ab31 | | | 595 | const sorted = |
| b69ab31 | | | 596 | set === undefined |
| b69ab31 | | | 597 | ? this.sortAsc(this, {gap: false}).reverse() |
| b69ab31 | | | 598 | : this.sortAsc(HashSet.fromHashes(set)).reverse(); |
| b69ab31 | | | 599 | const renderSet = new Set<Hash>(sorted); |
| b69ab31 | | | 600 | // Reserve a column for the public branch. |
| b69ab31 | | | 601 | for (const hash of sorted) { |
| b69ab31 | | | 602 | if (this.get(hash)?.phase === 'public') { |
| b69ab31 | | | 603 | yield ['reserve', hash]; |
| b69ab31 | | | 604 | break; |
| b69ab31 | | | 605 | } |
| b69ab31 | | | 606 | } |
| b69ab31 | | | 607 | // Render row by row. The main complexity is to figure out the "ancestors", |
| b69ab31 | | | 608 | // especially when the provided `set` is a subset of the dag. |
| b69ab31 | | | 609 | for (const hash of sorted) { |
| b69ab31 | | | 610 | const info = nullthrows(this.get(hash)); |
| b69ab31 | | | 611 | const parents: ReadonlyArray<Hash> = info?.parents ?? []; |
| b69ab31 | | | 612 | // directParents: solid edges |
| b69ab31 | | | 613 | // indirectParents: dashed edges |
| b69ab31 | | | 614 | // anonymousParents: ----"~" |
| b69ab31 | | | 615 | const {directParents, indirectParents, anonymousParents} = group(parents, p => { |
| b69ab31 | | | 616 | if (renderSet.has(p)) { |
| b69ab31 | | | 617 | return 'directParents'; |
| b69ab31 | | | 618 | } else if (this.has(p)) { |
| b69ab31 | | | 619 | return 'indirectParents'; |
| b69ab31 | | | 620 | } else { |
| b69ab31 | | | 621 | return 'anonymousParents'; |
| b69ab31 | | | 622 | } |
| b69ab31 | | | 623 | }); |
| b69ab31 | | | 624 | let typedParents: Ancestor[] = (directParents ?? []).map(p => { |
| b69ab31 | | | 625 | // We use 'info.ancestors' to fake ancestors as directParents. |
| b69ab31 | | | 626 | // Convert them to real ancestors so dashed lines are used. |
| b69ab31 | | | 627 | const type = info?.ancestors?.includes(p) ? AncestorType.Ancestor : AncestorType.Parent; |
| b69ab31 | | | 628 | return new Ancestor({type, hash: p}); |
| b69ab31 | | | 629 | }); |
| b69ab31 | | | 630 | if (anonymousParents != null && anonymousParents.length > 0 && info.ancestors == null) { |
| b69ab31 | | | 631 | typedParents.push(new Ancestor({type: AncestorType.Anonymous, hash: undefined})); |
| b69ab31 | | | 632 | } |
| b69ab31 | | | 633 | if (indirectParents != null && indirectParents.length > 0) { |
| b69ab31 | | | 634 | // Indirect parents might connect to "renderSet". Calculate it. |
| b69ab31 | | | 635 | // This can be expensive. |
| b69ab31 | | | 636 | // PERF: This is currently a dumb implementation and can probably be optimized. |
| b69ab31 | | | 637 | const grandParents = this.heads( |
| b69ab31 | | | 638 | this.ancestors(this.ancestors(indirectParents).intersect(renderSet)), |
| b69ab31 | | | 639 | ); |
| b69ab31 | | | 640 | // Exclude duplication with faked grand parents, since they are already in typedParents. |
| b69ab31 | | | 641 | const newGrandParents = grandParents.subtract(directParents); |
| b69ab31 | | | 642 | typedParents = typedParents.concat( |
| b69ab31 | | | 643 | newGrandParents.toArray().map(p => new Ancestor({type: AncestorType.Ancestor, hash: p})), |
| b69ab31 | | | 644 | ); |
| b69ab31 | | | 645 | } |
| b69ab31 | | | 646 | if (parents.length > 0 && typedParents.length === 0) { |
| b69ab31 | | | 647 | // The commit has parents but typedParents is empty (ex. (::indirect & renderSet) is empty). |
| b69ab31 | | | 648 | // Add an anonymous parent to indicate the commit is not a root. |
| b69ab31 | | | 649 | typedParents.push(new Ancestor({type: AncestorType.Anonymous, hash: undefined})); |
| b69ab31 | | | 650 | } |
| b69ab31 | | | 651 | yield ['row', [info, typedParents]]; |
| b69ab31 | | | 652 | } |
| b69ab31 | | | 653 | } |
| b69ab31 | | | 654 | |
| b69ab31 | | | 655 | /** |
| b69ab31 | | | 656 | * Render the dag in ASCII for debugging purpose. |
| b69ab31 | | | 657 | * If `set` is provided, only render a subset of the graph. |
| b69ab31 | | | 658 | */ |
| b69ab31 | | | 659 | renderAscii(set?: SetLike): string { |
| b69ab31 | | | 660 | const renderer = new TextRenderer(); |
| b69ab31 | | | 661 | const renderedRows = ['\n']; |
| b69ab31 | | | 662 | for (const [kind, data] of this.dagWalkerForRendering(set)) { |
| b69ab31 | | | 663 | if (kind === 'reserve') { |
| b69ab31 | | | 664 | renderer.reserve(data); |
| b69ab31 | | | 665 | } else { |
| b69ab31 | | | 666 | const [info, typedParents] = data; |
| b69ab31 | | | 667 | const {hash, title, author, date} = info; |
| b69ab31 | | | 668 | const message = |
| b69ab31 | | | 669 | [hash, title, author, date.valueOf() < 1000 ? '' : date.toISOString()] |
| b69ab31 | | | 670 | .join(' ') |
| b69ab31 | | | 671 | .trimEnd() + '\n'; |
| b69ab31 | | | 672 | const glyph = info?.isDot ? '@' : info?.successorInfo == null ? 'o' : 'x'; |
| b69ab31 | | | 673 | renderedRows.push(renderer.nextRow(info.hash, typedParents, message, glyph)); |
| b69ab31 | | | 674 | } |
| b69ab31 | | | 675 | } |
| b69ab31 | | | 676 | return renderedRows.join('').trimEnd(); |
| b69ab31 | | | 677 | } |
| b69ab31 | | | 678 | |
| b69ab31 | | | 679 | /** Provided extra fields for debugging use-case. For now, this includes an ASCII graph. */ |
| b69ab31 | | | 680 | getDebugState(): {rendered: Array<string>} { |
| b69ab31 | | | 681 | const rendered = this.renderAscii().split('\n'); |
| b69ab31 | | | 682 | return {rendered}; |
| b69ab31 | | | 683 | } |
| b69ab31 | | | 684 | } |
| b69ab31 | | | 685 | |
| b69ab31 | | | 686 | const rootsCache = new LRU(1000); |
| b69ab31 | | | 687 | const headsCache = new LRU(1000); |
| b69ab31 | | | 688 | const allCache = new LRU(1000); |
| b69ab31 | | | 689 | const subsetForRenderingCache = new LRU(1000); |
| b69ab31 | | | 690 | const defaultSortAscIndexCache = new LRU(1000); |
| b69ab31 | | | 691 | const renderToRowsCache = new LRU(1000); |
| b69ab31 | | | 692 | |
| b69ab31 | | | 693 | type NameMapEntry = [string, HashPriRecord]; |
| b69ab31 | | | 694 | |
| b69ab31 | | | 695 | /** Extract the (name, hash, pri) information for insertion and deletion. */ |
| b69ab31 | | | 696 | function infoToNameMapEntries(info: DagCommitInfo): Array<NameMapEntry> { |
| b69ab31 | | | 697 | // Priority, highest to lowest: |
| b69ab31 | | | 698 | // - full hash (handled by dag.resolve()) |
| b69ab31 | | | 699 | // - ".", the working parent |
| b69ab31 | | | 700 | // - namespaces.singlenode lookup |
| b69ab31 | | | 701 | // - 10: bookmarks |
| b69ab31 | | | 702 | // - 55: remotebookmarks (ex. "remote/main") |
| b69ab31 | | | 703 | // - 60: hoistednames (ex. "main" without "remote/") |
| b69ab31 | | | 704 | // - 70: phrevset (ex. "Dxxx"), but we skip it here due to lack |
| b69ab31 | | | 705 | // of access to the code review abstraction. |
| b69ab31 | | | 706 | // - partial hash (handled by dag.resolve()) |
| b69ab31 | | | 707 | const result: Array<NameMapEntry> = []; |
| b69ab31 | | | 708 | const {hash, isDot, bookmarks, remoteBookmarks} = info; |
| b69ab31 | | | 709 | if (isDot) { |
| b69ab31 | | | 710 | result.push(['.', HashPriRecord({hash, priority: 1})]); |
| b69ab31 | | | 711 | } |
| b69ab31 | | | 712 | bookmarks.forEach(b => result.push([b, HashPriRecord({hash, priority: 10})])); |
| b69ab31 | | | 713 | remoteBookmarks.forEach(rb => { |
| b69ab31 | | | 714 | result.push([rb, HashPriRecord({hash, priority: 55})]); |
| b69ab31 | | | 715 | const split = splitOnce(rb, '/')?.[1]; |
| b69ab31 | | | 716 | if (split) { |
| b69ab31 | | | 717 | result.push([split, HashPriRecord({hash, priority: 60})]); |
| b69ab31 | | | 718 | } |
| b69ab31 | | | 719 | }); |
| b69ab31 | | | 720 | return result; |
| b69ab31 | | | 721 | } |
| b69ab31 | | | 722 | |
| b69ab31 | | | 723 | /** Return the new NameMap after inserting or deleting `infos`. */ |
| b69ab31 | | | 724 | function calculateNewNameMap( |
| b69ab31 | | | 725 | map: NameMap, |
| b69ab31 | | | 726 | deleteInfos: Iterable<Readonly<DagCommitInfo>>, |
| b69ab31 | | | 727 | insertInfos: Iterable<Readonly<DagCommitInfo>>, |
| b69ab31 | | | 728 | ): NameMap { |
| b69ab31 | | | 729 | return map.withMutations(mut => { |
| b69ab31 | | | 730 | let map = mut; |
| b69ab31 | | | 731 | for (const info of deleteInfos) { |
| b69ab31 | | | 732 | const entries = infoToNameMapEntries(info); |
| b69ab31 | | | 733 | for (const [name, hashPri] of entries) { |
| b69ab31 | | | 734 | map = map.removeIn([name, hashPri]); |
| b69ab31 | | | 735 | if (map.get(name)?.isEmpty()) { |
| b69ab31 | | | 736 | map = map.remove(name); |
| b69ab31 | | | 737 | } |
| b69ab31 | | | 738 | } |
| b69ab31 | | | 739 | } |
| b69ab31 | | | 740 | for (const info of insertInfos) { |
| b69ab31 | | | 741 | const entries = infoToNameMapEntries(info); |
| b69ab31 | | | 742 | for (const [name, hashPri] of entries) { |
| b69ab31 | | | 743 | const set = map.get(name); |
| b69ab31 | | | 744 | if (set === undefined) { |
| b69ab31 | | | 745 | map = map.set(name, ImSet<HashPriRecord>([hashPri])); |
| b69ab31 | | | 746 | } else { |
| b69ab31 | | | 747 | map = map.set(name, set.add(hashPri)); |
| b69ab31 | | | 748 | } |
| b69ab31 | | | 749 | } |
| b69ab31 | | | 750 | } |
| b69ab31 | | | 751 | return map; |
| b69ab31 | | | 752 | }); |
| b69ab31 | | | 753 | } |
| b69ab31 | | | 754 | |
| b69ab31 | | | 755 | /** Decide whether `hash` looks like a hash prefix. */ |
| b69ab31 | | | 756 | function shouldPrefixMatch(hash: Hash): boolean { |
| b69ab31 | | | 757 | // No prefix match for full hashes. |
| b69ab31 | | | 758 | if (hash.length >= 40) { |
| b69ab31 | | | 759 | return false; |
| b69ab31 | | | 760 | } |
| b69ab31 | | | 761 | // No prefix match for non-hex hashes. |
| b69ab31 | | | 762 | return /^[0-9a-f]+$/.test(hash); |
| b69ab31 | | | 763 | } |
| b69ab31 | | | 764 | |
| b69ab31 | | | 765 | type NameMap = ImMap<string, ImSet<HashPriRecord>>; |
| b69ab31 | | | 766 | |
| b69ab31 | | | 767 | type CommitDagProps = { |
| b69ab31 | | | 768 | commitDag: BaseDag<DagCommitInfo>; |
| b69ab31 | | | 769 | mutationDag: MutationDag; |
| b69ab31 | | | 770 | // derived from Info, for fast "resolve" lookup. name -> hashpri |
| b69ab31 | | | 771 | nameMap: NameMap; |
| b69ab31 | | | 772 | nextSeqNumber: number; |
| b69ab31 | | | 773 | }; |
| b69ab31 | | | 774 | |
| b69ab31 | | | 775 | const CommitDagRecord = Record<CommitDagProps>({ |
| b69ab31 | | | 776 | commitDag: new BaseDag(), |
| b69ab31 | | | 777 | mutationDag: new MutationDag(), |
| b69ab31 | | | 778 | nameMap: ImMap() as NameMap, |
| b69ab31 | | | 779 | nextSeqNumber: 0, |
| b69ab31 | | | 780 | }); |
| b69ab31 | | | 781 | |
| b69ab31 | | | 782 | type CommitDagRecord = RecordOf<CommitDagProps>; |
| b69ab31 | | | 783 | |
| b69ab31 | | | 784 | type HashPriProps = { |
| b69ab31 | | | 785 | hash: Hash; |
| b69ab31 | | | 786 | // for 'resolve' use-case; lower number = higher priority |
| b69ab31 | | | 787 | priority: number; |
| b69ab31 | | | 788 | }; |
| b69ab31 | | | 789 | const HashPriRecord = Record<HashPriProps>({hash: '', priority: 0}); |
| b69ab31 | | | 790 | type HashPriRecord = RecordOf<HashPriProps>; |
| b69ab31 | | | 791 | |
| b69ab31 | | | 792 | const EMPTY_DAG_RECORD = CommitDagRecord(); |
| b69ab31 | | | 793 | |
| b69ab31 | | | 794 | /** 'Hash' prefix for rebase successor in preview. */ |
| b69ab31 | | | 795 | export const REBASE_SUCC_PREFIX = 'OPTIMISTIC_REBASE_SUCC:'; |
| b69ab31 | | | 796 | |
| b69ab31 | | | 797 | /** Default 'compare' function for sortAsc. */ |
| b69ab31 | | | 798 | const sortAscCompare = (a: DagCommitInfo, b: DagCommitInfo) => { |
| b69ab31 | | | 799 | // Consider phase. Public last. For example, when sorting this dag |
| b69ab31 | | | 800 | // (used by tests): |
| b69ab31 | | | 801 | // |
| b69ab31 | | | 802 | // ```plain |
| b69ab31 | | | 803 | // o z Commit Z author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 804 | // │ |
| b69ab31 | | | 805 | // o y Commit Y author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 806 | // │ |
| b69ab31 | | | 807 | // o x Commit X author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 808 | // ╭─╯ |
| b69ab31 | | | 809 | // o 2 another public branch author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 810 | // ├─╮ |
| b69ab31 | | | 811 | // ╷ │ |
| b69ab31 | | | 812 | // ╷ ~ |
| b69ab31 | | | 813 | // ╷ |
| b69ab31 | | | 814 | // ╷ @ e Commit E author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 815 | // ╷ │ |
| b69ab31 | | | 816 | // ╷ o d Commit D author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 817 | // ╷ │ |
| b69ab31 | | | 818 | // ╷ o c Commit C author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 819 | // ╷ │ |
| b69ab31 | | | 820 | // ╷ o b Commit B author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 821 | // ╷ │ |
| b69ab31 | | | 822 | // ╷ o a Commit A author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 823 | // ╭─╯ |
| b69ab31 | | | 824 | // o 1 some public base author 2024-01-03T21:10:04.674Z |
| b69ab31 | | | 825 | // │ |
| b69ab31 | | | 826 | // ~ |
| b69ab31 | | | 827 | // ``` |
| b69ab31 | | | 828 | // |
| b69ab31 | | | 829 | // The desired order is [1, a, b, c, d, e, 2, x, y, z] that matches |
| b69ab31 | | | 830 | // the reversed rendering order. 'a' (draft) is before '2' (public). |
| b69ab31 | | | 831 | if (a.phase !== b.phase) { |
| b69ab31 | | | 832 | return a.phase === 'public' ? 1 : -1; |
| b69ab31 | | | 833 | } |
| b69ab31 | | | 834 | // Consider seqNumber (insertion order during preview calculation). |
| b69ab31 | | | 835 | if (a.seqNumber != null && b.seqNumber != null) { |
| b69ab31 | | | 836 | const seqDelta = b.seqNumber - a.seqNumber; |
| b69ab31 | | | 837 | if (seqDelta !== 0) { |
| b69ab31 | | | 838 | return seqDelta; |
| b69ab31 | | | 839 | } |
| b69ab31 | | | 840 | } |
| b69ab31 | | | 841 | // Sort by date. |
| b69ab31 | | | 842 | const timeDelta = a.date.getTime() - b.date.getTime(); |
| b69ab31 | | | 843 | if (timeDelta !== 0) { |
| b69ab31 | | | 844 | return timeDelta; |
| b69ab31 | | | 845 | } |
| b69ab31 | | | 846 | // Always break ties even if timestamp is the same. |
| b69ab31 | | | 847 | return a.hash < b.hash ? 1 : -1; |
| b69ab31 | | | 848 | }; |
| b69ab31 | | | 849 | |
| b69ab31 | | | 850 | export {DagCommitInfo}; |