Fossil SCM

Continued work on pass 8. Completed the handling of backward branches, file level analysis and splitting them. Extended changesets with the necessary methods to the predecessor data and proper per-revision maps.

aku 2007-11-22 04:21 trunk
Commit e50f9ed55ec4e44c90e50fad10245879c2f2b7da
--- tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
+++ tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
@@ -121,11 +121,11 @@
121121
# looking at all changesets to create a sql which selects all
122122
# the branch changesets from the state in one go instead of
123123
# having to check each changeset separately. Consider this
124124
# later, get the pass working first.
125125
#
126
- # NOTE 2: Might we even be able to select the retrograde
126
+ # NOTE 2: Might we even be able to select the backward branch
127127
# changesets too ?
128128
129129
foreach cset [$graph nodes] {
130130
if {![$cset isbranch]} continue
131131
CheckAndBreakBackwardBranch $graph $cset
@@ -135,11 +135,49 @@
135135
136136
proc CheckAndBreakBackwardBranch {graph cset} {
137137
while {[IsABackwardBranch $graph $cset]} {
138138
log write 5 breakacycle "Breaking backward branch changeset <[$cset id]>"
139139
140
- break
140
+ # Knowing that the branch is backward we now look at the
141
+ # individual revisions in the changeset and determine
142
+ # which of them are responsible for the overlap. This
143
+ # allows us to split them into two sets, one of
144
+ # non-overlapping revisions, and of overlapping ones. Each
145
+ # induces a new changeset, and the second may still be
146
+ # backward and need further splitting. Hence the looping.
147
+ #
148
+ # The border used for the split is the minimal commit
149
+ # position among the minimal sucessor commit positions for
150
+ # the revisions in the changeset.
151
+
152
+ # Note that individual revisions may not have revision
153
+ # changesets are predecessors and/or successors, leaving
154
+ # the limits partially or completely undefined.
155
+
156
+ # limits : dict (revision -> list (max predecessor commit, min sucessor commit))
157
+
158
+ ComputeLimits $cset limits border
159
+
160
+ log write 6 breakacycle "At commit position border $border"
161
+
162
+ # Then we sort the file level items based on there they
163
+ # sit relative to the border into before and after the
164
+ # border.
165
+
166
+ SplitRevisions $limits normalrevisions backwardrevisions
167
+
168
+ set replacements [project::rev split $cset $normalrevisions $backwardrevisions]
169
+ cyclebreaker replace $graph $cset $replacements
170
+
171
+ # At last check that the normal frament is indeed not
172
+ # backward, and iterate over the possibly still backward
173
+ # second fragment.
174
+
175
+ struct::list assign $replacements normal backward
176
+ if {[IsABackwardBranch $dg $normal]} { trouble internal "The normal fragment is unexpectedly a backward branch" }
177
+
178
+ set cset $backward
141179
}
142180
return
143181
}
144182
145183
proc IsABackwardBranch {dg cset} {
@@ -182,10 +220,89 @@
182220
[myproc ValidPosition]]
183221
}
184222
185223
proc ToPosition {cset} { $cset pos }
186224
proc ValidPosition {pos} { expr {$pos ne ""} }
225
+
226
+ proc ComputeLimits {cset lv bv} {
227
+ upvar 1 $lv thelimits $bv border
228
+
229
+ # Initialize the boundaries for all revisions.
230
+
231
+ array set limits {}
232
+ foreach revision [$cset revisions] {
233
+ set limits($revision) {0 {}}
234
+ }
235
+
236
+ # Compute and store the maximal predecessors per revision
237
+
238
+ foreach {revision csets} [$cset predecessormap] {
239
+ set s [Positions $csets]
240
+ if {![llength $s]} continue
241
+ set limits($revision) [lreplace $limits($revision) 0 0 [max $s]]
242
+ }
243
+
244
+ # Compute and store the minimal successors per revision
245
+
246
+ foreach {revision csets} [$cset successormap] {
247
+ set s [Positions $csets]
248
+ if {![llength $s]} continue
249
+ set limits($revision) [lreplace $limits($revision) 1 1 [min $s]]
250
+ }
251
+
252
+ # Check that the ordering at the file level is correct. We
253
+ # cannot have backward ordering per revision, or something is
254
+ # wrong.
255
+
256
+ foreach revision [array names limits] {
257
+ struct::list assign $limits($revision) maxp mins
258
+ # Handle min successor position "" as representing infinity
259
+ if {$mins eq ""} continue
260
+ if {$maxp < $mins} continue
261
+
262
+ trouble internal "Branch revision $revision is backward at file level ($maxp >= $mins)"
263
+ }
264
+
265
+ # Save the limits for the splitter, and compute the border at
266
+ # which to split as the minimum of all minimal successor
267
+ # positions.
268
+
269
+ set thelimits [array get limits]
270
+ set border [min [struct::list filter [struct::list map [Values $thelimits] \
271
+ [myproc MinSuccessorPosition]] \
272
+ [myproc ValidPosition]]]
273
+ return
274
+ }
275
+
276
+ proc Values {dict} {
277
+ set res {}
278
+ foreach {k v} $dict { lappend res $v }
279
+ return $res
280
+ }
281
+
282
+ proc MinSuccessorPosition {item} { lindex $item 1 }
283
+
284
+ proc SplitRevisions {limits nv bv} {
285
+ upvar 1 $nv normalrevisions $bv backwardrevisions
286
+
287
+ set normalrevisions {}
288
+ set backwardrevisions {}
289
+
290
+ foreach {rev v} $limits {
291
+ struct::list assign $v maxp mins
292
+ if {$maxp >= $border} {
293
+ lappend backwardrevisions $rev
294
+ } else {
295
+ lappend normalrevisions $rev
296
+ }
297
+ }
298
+
299
+ if {![llength $normalrevisions]} { trouble internal "Set of normal revisions is empty" }
300
+ if {![llength $backwardrevisions]} { trouble internal "Set of backward revisions is empty" }
301
+ return
302
+ }
303
+
187304
188305
# # ## ### ##### ######## #############
189306
190307
proc SaveOrder {cset pos} {
191308
}
192309
--- tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
+++ tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
@@ -121,11 +121,11 @@
121 # looking at all changesets to create a sql which selects all
122 # the branch changesets from the state in one go instead of
123 # having to check each changeset separately. Consider this
124 # later, get the pass working first.
125 #
126 # NOTE 2: Might we even be able to select the retrograde
127 # changesets too ?
128
129 foreach cset [$graph nodes] {
130 if {![$cset isbranch]} continue
131 CheckAndBreakBackwardBranch $graph $cset
@@ -135,11 +135,49 @@
135
136 proc CheckAndBreakBackwardBranch {graph cset} {
137 while {[IsABackwardBranch $graph $cset]} {
138 log write 5 breakacycle "Breaking backward branch changeset <[$cset id]>"
139
140 break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141 }
142 return
143 }
144
145 proc IsABackwardBranch {dg cset} {
@@ -182,10 +220,89 @@
182 [myproc ValidPosition]]
183 }
184
185 proc ToPosition {cset} { $cset pos }
186 proc ValidPosition {pos} { expr {$pos ne ""} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
188 # # ## ### ##### ######## #############
189
190 proc SaveOrder {cset pos} {
191 }
192
--- tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
+++ tools/cvs2fossil/lib/c2f_pbreakacycle.tcl
@@ -121,11 +121,11 @@
121 # looking at all changesets to create a sql which selects all
122 # the branch changesets from the state in one go instead of
123 # having to check each changeset separately. Consider this
124 # later, get the pass working first.
125 #
126 # NOTE 2: Might we even be able to select the backward branch
127 # changesets too ?
128
129 foreach cset [$graph nodes] {
130 if {![$cset isbranch]} continue
131 CheckAndBreakBackwardBranch $graph $cset
@@ -135,11 +135,49 @@
135
136 proc CheckAndBreakBackwardBranch {graph cset} {
137 while {[IsABackwardBranch $graph $cset]} {
138 log write 5 breakacycle "Breaking backward branch changeset <[$cset id]>"
139
140 # Knowing that the branch is backward we now look at the
141 # individual revisions in the changeset and determine
142 # which of them are responsible for the overlap. This
143 # allows us to split them into two sets, one of
144 # non-overlapping revisions, and of overlapping ones. Each
145 # induces a new changeset, and the second may still be
146 # backward and need further splitting. Hence the looping.
147 #
148 # The border used for the split is the minimal commit
149 # position among the minimal sucessor commit positions for
150 # the revisions in the changeset.
151
152 # Note that individual revisions may not have revision
153 # changesets are predecessors and/or successors, leaving
154 # the limits partially or completely undefined.
155
156 # limits : dict (revision -> list (max predecessor commit, min sucessor commit))
157
158 ComputeLimits $cset limits border
159
160 log write 6 breakacycle "At commit position border $border"
161
162 # Then we sort the file level items based on there they
163 # sit relative to the border into before and after the
164 # border.
165
166 SplitRevisions $limits normalrevisions backwardrevisions
167
168 set replacements [project::rev split $cset $normalrevisions $backwardrevisions]
169 cyclebreaker replace $graph $cset $replacements
170
171 # At last check that the normal frament is indeed not
172 # backward, and iterate over the possibly still backward
173 # second fragment.
174
175 struct::list assign $replacements normal backward
176 if {[IsABackwardBranch $dg $normal]} { trouble internal "The normal fragment is unexpectedly a backward branch" }
177
178 set cset $backward
179 }
180 return
181 }
182
183 proc IsABackwardBranch {dg cset} {
@@ -182,10 +220,89 @@
220 [myproc ValidPosition]]
221 }
222
223 proc ToPosition {cset} { $cset pos }
224 proc ValidPosition {pos} { expr {$pos ne ""} }
225
226 proc ComputeLimits {cset lv bv} {
227 upvar 1 $lv thelimits $bv border
228
229 # Initialize the boundaries for all revisions.
230
231 array set limits {}
232 foreach revision [$cset revisions] {
233 set limits($revision) {0 {}}
234 }
235
236 # Compute and store the maximal predecessors per revision
237
238 foreach {revision csets} [$cset predecessormap] {
239 set s [Positions $csets]
240 if {![llength $s]} continue
241 set limits($revision) [lreplace $limits($revision) 0 0 [max $s]]
242 }
243
244 # Compute and store the minimal successors per revision
245
246 foreach {revision csets} [$cset successormap] {
247 set s [Positions $csets]
248 if {![llength $s]} continue
249 set limits($revision) [lreplace $limits($revision) 1 1 [min $s]]
250 }
251
252 # Check that the ordering at the file level is correct. We
253 # cannot have backward ordering per revision, or something is
254 # wrong.
255
256 foreach revision [array names limits] {
257 struct::list assign $limits($revision) maxp mins
258 # Handle min successor position "" as representing infinity
259 if {$mins eq ""} continue
260 if {$maxp < $mins} continue
261
262 trouble internal "Branch revision $revision is backward at file level ($maxp >= $mins)"
263 }
264
265 # Save the limits for the splitter, and compute the border at
266 # which to split as the minimum of all minimal successor
267 # positions.
268
269 set thelimits [array get limits]
270 set border [min [struct::list filter [struct::list map [Values $thelimits] \
271 [myproc MinSuccessorPosition]] \
272 [myproc ValidPosition]]]
273 return
274 }
275
276 proc Values {dict} {
277 set res {}
278 foreach {k v} $dict { lappend res $v }
279 return $res
280 }
281
282 proc MinSuccessorPosition {item} { lindex $item 1 }
283
284 proc SplitRevisions {limits nv bv} {
285 upvar 1 $nv normalrevisions $bv backwardrevisions
286
287 set normalrevisions {}
288 set backwardrevisions {}
289
290 foreach {rev v} $limits {
291 struct::list assign $v maxp mins
292 if {$maxp >= $border} {
293 lappend backwardrevisions $rev
294 } else {
295 lappend normalrevisions $rev
296 }
297 }
298
299 if {![llength $normalrevisions]} { trouble internal "Set of normal revisions is empty" }
300 if {![llength $backwardrevisions]} { trouble internal "Set of backward revisions is empty" }
301 return
302 }
303
304
305 # # ## ### ##### ######## #############
306
307 proc SaveOrder {cset pos} {
308 }
309
--- tools/cvs2fossil/lib/c2f_prev.tcl
+++ tools/cvs2fossil/lib/c2f_prev.tcl
@@ -66,10 +66,22 @@
6666
return [expr {($mytype eq "sym") &&
6767
($mybranchcode == [state one {
6868
SELECT type FROM symbol WHERE sid = $mysrcid
6969
}])}]
7070
}
71
+
72
+ method successormap {} {
73
+ # NOTE / FUTURE: Possible bottleneck.
74
+ array set tmp {}
75
+ foreach {rev children} [$self nextmap] {
76
+ foreach child $children {
77
+ lappend tmp($rev) $myrevmap($child)
78
+ }
79
+ set tmp($rev) [lsort -unique $tmp($rev)]
80
+ }
81
+ return [array get tmp]
82
+ }
7183
7284
method successors {} {
7385
# NOTE / FUTURE: Possible bottleneck.
7486
set csets {}
7587
foreach {_ children} [$self nextmap] {
@@ -77,18 +89,38 @@
7789
lappend csets $myrevmap($child)
7890
}
7991
}
8092
return [lsort -unique $csets]
8193
}
94
+
95
+ method predecessormap {} {
96
+ # NOTE / FUTURE: Possible bottleneck.
97
+ array set tmp {}
98
+ foreach {rev children} [$self premap] {
99
+ foreach child $children {
100
+ lappend tmp($rev) $myrevmap($child)
101
+ }
102
+ set tmp($rev) [lsort -unique $tmp($rev)]
103
+ }
104
+ return [array get tmp]
105
+ }
82106
83107
# revision -> list (revision)
84108
method nextmap {} {
85109
if {[llength $mynextmap]} { return $mynextmap }
86110
PullSuccessorRevisions tmp $myrevisions
87111
set mynextmap [array get tmp]
88112
return $mynextmap
89113
}
114
+
115
+ # revision -> list (revision)
116
+ method premap {} {
117
+ if {[llength $mypremap]} { return $mypremap }
118
+ PullPredecessorRevisions tmp $myrevisions
119
+ set mypremap [array get tmp]
120
+ return $mypremap
121
+ }
90122
91123
method breakinternaldependencies {} {
92124
# This method inspects the changesets for internal
93125
# dependencies. Nothing is done if there are no
94126
# such. Otherwise the changeset is split into a set of
@@ -303,10 +335,14 @@
303335
# from.
304336
variable mysrcid {} ; # Id of the metadata or symbol the cset
305337
# is based on.
306338
variable myrevisions {} ; # List of the file level revisions in
307339
# the cset.
340
+ variable mypremap {} ; # Dictionary mapping from the revisions
341
+ # to their predecessors. Cache to avoid
342
+ # loading this from the state more than
343
+ # once.
308344
variable mynextmap {} ; # Dictionary mapping from the revisions
309345
# to their successors. Cache to avoid
310346
# loading this from the state more than
311347
# once.
312348
variable mypos {} ; # Commit position of the changeset, if
@@ -393,10 +429,35 @@
393429
trouble internal "Revision $rid depends on itself."
394430
}
395431
lappend dependencies($rid) $child
396432
}
397433
}
434
+
435
+ proc PullPredecessorRevisions {dv revisions} {
436
+ upvar 1 $dv dependencies
437
+ set theset ('[join $revisions {','}]')
438
+
439
+ foreach {rid parent} [state run "
440
+ -- Primary parent, can be in different LOD for first in a branch
441
+ SELECT R.rid, R.parent
442
+ FROM revision R
443
+ WHERE R.rid IN $theset
444
+ AND R.parent IS NOT NULL
445
+ UNION
446
+ -- Transition trunk to NTDB
447
+ SELECT R.rid, R.dbparent
448
+ FROM revision R
449
+ WHERE R.rid IN $theset
450
+ AND R.dbparent IS NOT NULL
451
+ "] {
452
+ # Consider moving this to the integrity module.
453
+ if {$rid == $parent} {
454
+ trouble internal "Revision $rid depends on itself."
455
+ }
456
+ lappend dependencies($rid) $parent
457
+ }
458
+ }
398459
399460
proc InitializeBreakState {revisions} {
400461
upvar 1 pos pos cross cross range range depc depc delta delta \
401462
dependencies dependencies
402463
@@ -608,12 +669,13 @@
608669
typevariable mychangesets {} ; # List of all known changesets.
609670
typevariable myrevmap -array {} ; # Map from revisions to their changeset.
610671
typevariable myidmap -array {} ; # Map from changeset id to changeset.
611672
typevariable mybranchcode {} ; # Local copy of project::sym/mybranch.
612673
613
- typemethod all {} { return $mychangesets }
614
- typemethod of {id} { return $myidmap($id) }
674
+ typemethod all {} { return $mychangesets }
675
+ typemethod of {id} { return $myidmap($id) }
676
+ typemethod ofrev {id} { return $myrevmap($id) }
615677
616678
typeconstructor {
617679
set mybranchcode [project::sym branch]
618680
return
619681
}
620682
--- tools/cvs2fossil/lib/c2f_prev.tcl
+++ tools/cvs2fossil/lib/c2f_prev.tcl
@@ -66,10 +66,22 @@
66 return [expr {($mytype eq "sym") &&
67 ($mybranchcode == [state one {
68 SELECT type FROM symbol WHERE sid = $mysrcid
69 }])}]
70 }
 
 
 
 
 
 
 
 
 
 
 
 
71
72 method successors {} {
73 # NOTE / FUTURE: Possible bottleneck.
74 set csets {}
75 foreach {_ children} [$self nextmap] {
@@ -77,18 +89,38 @@
77 lappend csets $myrevmap($child)
78 }
79 }
80 return [lsort -unique $csets]
81 }
 
 
 
 
 
 
 
 
 
 
 
 
82
83 # revision -> list (revision)
84 method nextmap {} {
85 if {[llength $mynextmap]} { return $mynextmap }
86 PullSuccessorRevisions tmp $myrevisions
87 set mynextmap [array get tmp]
88 return $mynextmap
89 }
 
 
 
 
 
 
 
 
90
91 method breakinternaldependencies {} {
92 # This method inspects the changesets for internal
93 # dependencies. Nothing is done if there are no
94 # such. Otherwise the changeset is split into a set of
@@ -303,10 +335,14 @@
303 # from.
304 variable mysrcid {} ; # Id of the metadata or symbol the cset
305 # is based on.
306 variable myrevisions {} ; # List of the file level revisions in
307 # the cset.
 
 
 
 
308 variable mynextmap {} ; # Dictionary mapping from the revisions
309 # to their successors. Cache to avoid
310 # loading this from the state more than
311 # once.
312 variable mypos {} ; # Commit position of the changeset, if
@@ -393,10 +429,35 @@
393 trouble internal "Revision $rid depends on itself."
394 }
395 lappend dependencies($rid) $child
396 }
397 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
399 proc InitializeBreakState {revisions} {
400 upvar 1 pos pos cross cross range range depc depc delta delta \
401 dependencies dependencies
402
@@ -608,12 +669,13 @@
608 typevariable mychangesets {} ; # List of all known changesets.
609 typevariable myrevmap -array {} ; # Map from revisions to their changeset.
610 typevariable myidmap -array {} ; # Map from changeset id to changeset.
611 typevariable mybranchcode {} ; # Local copy of project::sym/mybranch.
612
613 typemethod all {} { return $mychangesets }
614 typemethod of {id} { return $myidmap($id) }
 
615
616 typeconstructor {
617 set mybranchcode [project::sym branch]
618 return
619 }
620
--- tools/cvs2fossil/lib/c2f_prev.tcl
+++ tools/cvs2fossil/lib/c2f_prev.tcl
@@ -66,10 +66,22 @@
66 return [expr {($mytype eq "sym") &&
67 ($mybranchcode == [state one {
68 SELECT type FROM symbol WHERE sid = $mysrcid
69 }])}]
70 }
71
72 method successormap {} {
73 # NOTE / FUTURE: Possible bottleneck.
74 array set tmp {}
75 foreach {rev children} [$self nextmap] {
76 foreach child $children {
77 lappend tmp($rev) $myrevmap($child)
78 }
79 set tmp($rev) [lsort -unique $tmp($rev)]
80 }
81 return [array get tmp]
82 }
83
84 method successors {} {
85 # NOTE / FUTURE: Possible bottleneck.
86 set csets {}
87 foreach {_ children} [$self nextmap] {
@@ -77,18 +89,38 @@
89 lappend csets $myrevmap($child)
90 }
91 }
92 return [lsort -unique $csets]
93 }
94
95 method predecessormap {} {
96 # NOTE / FUTURE: Possible bottleneck.
97 array set tmp {}
98 foreach {rev children} [$self premap] {
99 foreach child $children {
100 lappend tmp($rev) $myrevmap($child)
101 }
102 set tmp($rev) [lsort -unique $tmp($rev)]
103 }
104 return [array get tmp]
105 }
106
107 # revision -> list (revision)
108 method nextmap {} {
109 if {[llength $mynextmap]} { return $mynextmap }
110 PullSuccessorRevisions tmp $myrevisions
111 set mynextmap [array get tmp]
112 return $mynextmap
113 }
114
115 # revision -> list (revision)
116 method premap {} {
117 if {[llength $mypremap]} { return $mypremap }
118 PullPredecessorRevisions tmp $myrevisions
119 set mypremap [array get tmp]
120 return $mypremap
121 }
122
123 method breakinternaldependencies {} {
124 # This method inspects the changesets for internal
125 # dependencies. Nothing is done if there are no
126 # such. Otherwise the changeset is split into a set of
@@ -303,10 +335,14 @@
335 # from.
336 variable mysrcid {} ; # Id of the metadata or symbol the cset
337 # is based on.
338 variable myrevisions {} ; # List of the file level revisions in
339 # the cset.
340 variable mypremap {} ; # Dictionary mapping from the revisions
341 # to their predecessors. Cache to avoid
342 # loading this from the state more than
343 # once.
344 variable mynextmap {} ; # Dictionary mapping from the revisions
345 # to their successors. Cache to avoid
346 # loading this from the state more than
347 # once.
348 variable mypos {} ; # Commit position of the changeset, if
@@ -393,10 +429,35 @@
429 trouble internal "Revision $rid depends on itself."
430 }
431 lappend dependencies($rid) $child
432 }
433 }
434
435 proc PullPredecessorRevisions {dv revisions} {
436 upvar 1 $dv dependencies
437 set theset ('[join $revisions {','}]')
438
439 foreach {rid parent} [state run "
440 -- Primary parent, can be in different LOD for first in a branch
441 SELECT R.rid, R.parent
442 FROM revision R
443 WHERE R.rid IN $theset
444 AND R.parent IS NOT NULL
445 UNION
446 -- Transition trunk to NTDB
447 SELECT R.rid, R.dbparent
448 FROM revision R
449 WHERE R.rid IN $theset
450 AND R.dbparent IS NOT NULL
451 "] {
452 # Consider moving this to the integrity module.
453 if {$rid == $parent} {
454 trouble internal "Revision $rid depends on itself."
455 }
456 lappend dependencies($rid) $parent
457 }
458 }
459
460 proc InitializeBreakState {revisions} {
461 upvar 1 pos pos cross cross range range depc depc delta delta \
462 dependencies dependencies
463
@@ -608,12 +669,13 @@
669 typevariable mychangesets {} ; # List of all known changesets.
670 typevariable myrevmap -array {} ; # Map from revisions to their changeset.
671 typevariable myidmap -array {} ; # Map from changeset id to changeset.
672 typevariable mybranchcode {} ; # Local copy of project::sym/mybranch.
673
674 typemethod all {} { return $mychangesets }
675 typemethod of {id} { return $myidmap($id) }
676 typemethod ofrev {id} { return $myrevmap($id) }
677
678 typeconstructor {
679 set mybranchcode [project::sym branch]
680 return
681 }
682

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button