Fossil SCM

fossil-scm / src / diff.tcl
Blame History Raw 634 lines
1
# The "diff --tk" command outputs prepends a "set fossilcmd {...}" line
2
# to this file, then runs this file using "tclsh" in order to display the
3
# graphical diff in a separate window. A typical "set fossilcmd" line
4
# looks like this:
5
#
6
# set fossilcmd {| "./fossil" diff --tcl -i -v}
7
#
8
# This header comment is stripped off by the "mkbuiltin.c" program.
9
#
10
set prog {
11
package require Tk
12
13
array set CFG_light {
14
TITLE {Fossil Diff}
15
LN_COL_BG #dddddd
16
LN_COL_FG #444444
17
TXT_COL_BG #ffffff
18
TXT_COL_FG #000000
19
MKR_COL_BG #444444
20
MKR_COL_FG #dddddd
21
CHNG_BG #d0d0ff
22
ADD_BG #c0ffc0
23
RM_BG #ffc0c0
24
HR_FG #444444
25
HR_PAD_TOP 4
26
HR_PAD_BTM 8
27
FN_BG #444444
28
FN_FG #ffffff
29
FN_PAD 5
30
ERR_FG #ee0000
31
PADX 5
32
WIDTH 80
33
HEIGHT 45
34
LB_HEIGHT 25
35
}
36
37
array set CFG_dark {
38
TITLE {Fossil Diff}
39
LN_COL_BG #dddddd
40
LN_COL_FG #444444
41
TXT_COL_BG #3f3f3f
42
TXT_COL_FG #dcdccc
43
MKR_COL_BG #444444
44
MKR_COL_FG #dddddd
45
CHNG_BG #6a6afc
46
ADD_BG #57934c
47
RM_BG #ef6767
48
HR_FG #444444
49
HR_PAD_TOP 4
50
HR_PAD_BTM 8
51
FN_BG #5e5e5e
52
FN_FG #ffffff
53
FN_PAD 5
54
ERR_FG #ee0000
55
PADX 5
56
WIDTH 80
57
HEIGHT 45
58
LB_HEIGHT 25
59
}
60
61
array set CFG_arr {
62
0 CFG_light
63
1 CFG_dark
64
}
65
66
array set CFG [array get $CFG_arr($darkmode)]
67
68
if {![namespace exists ttk]} {
69
interp alias {} ::ttk::scrollbar {} ::scrollbar
70
interp alias {} ::ttk::menubutton {} ::menubutton
71
}
72
73
proc dehtml {x} {
74
set x [regsub -all {<[^>]*>} $x {}]
75
return [string map {&amp; & &lt; < &gt; > &#39; ' &quot; \"} $x]
76
}
77
78
proc cols {} {
79
return [list .lnA .txtA .mkr .lnB .txtB]
80
}
81
82
proc colType {c} {
83
regexp {[a-z]+} $c type
84
return $type
85
}
86
87
proc getLine {difftxt N iivar} {
88
upvar $iivar ii
89
if {$ii>=$N} {return -1}
90
set x [lindex $difftxt $ii]
91
incr ii
92
return $x
93
}
94
95
proc reloadDiff {} {
96
global fossilcmd difftxt
97
unset -nocomplain difftxt
98
set idx [.txtA index @0,0]
99
readDiffs $fossilcmd 1
100
update
101
viewDiff $idx
102
}
103
104
proc readDiffs {fossilcmd redo} {
105
global difftxt debug
106
if {![info exists difftxt]} {
107
if {$debug} {
108
puts "# [list open $fossilcmd r]"
109
flush stdout
110
}
111
if {[catch {
112
set in [open $fossilcmd r]
113
fconfigure $in -encoding utf-8
114
set difftxt [split [read $in] \n]
115
close $in
116
} msg]} {
117
if {$redo} {
118
tk_messageBox -type ok -title Error -message "Unable to refresh:\n$msg"
119
return 0
120
} else {
121
puts $msg
122
exit 1
123
}
124
}
125
}
126
set N [llength $difftxt]
127
set ii 0
128
set nDiffs 0
129
set n1 0
130
set n2 0
131
array set widths {txt 3 ln 3 mkr 1}
132
if {$redo} {
133
foreach c [cols] {$c config -state normal}
134
.lnA delete 1.0 end
135
.txtA delete 1.0 end
136
.lnB delete 1.0 end
137
.txtB delete 1.0 end
138
.mkr delete 1.0 end
139
.wfiles.lb delete 0 end
140
}
141
142
143
set fromIndex [lsearch -glob $fossilcmd *-from]
144
set toIndex [lsearch -glob $fossilcmd *-to]
145
set branchIndex [lsearch -glob $fossilcmd *-branch]
146
set checkinIndex [lsearch -glob $fossilcmd *-checkin]
147
if {[lsearch -glob $fossilcmd *-label]>=0
148
|| [lsearch {xdiff fdiff merge-info} [lindex $fossilcmd 2]]>=0
149
} {
150
set fA {}
151
set fB {}
152
} else {
153
if {[string match *?--external-baseline* $fossilcmd]} {
154
set fA {external baseline}
155
} else {
156
set fA {base check-in}
157
}
158
set fB {current check-out}
159
if {$fromIndex > -1} {
160
set fA [lindex $fossilcmd $fromIndex+1]
161
}
162
if {$toIndex > -1} {
163
set fB [lindex $fossilcmd $toIndex+1]
164
}
165
if {$branchIndex > -1} {
166
set fA "branch point"; set fB "leaf of branch '[lindex $fossilcmd $branchIndex+1]'"
167
}
168
if {$checkinIndex > -1} {
169
set fA "primary parent"; set fB [lindex $fossilcmd $checkinIndex+1]
170
}
171
}
172
173
while {[set line [getLine $difftxt $N ii]] != -1} {
174
switch -- [lindex $line 0] {
175
FILE {
176
incr nDiffs
177
foreach wx [list [string length $n1] [string length $n2]] {
178
if {$wx>$widths(ln)} {set widths(ln) $wx}
179
}
180
.lnA insert end \n fn \n -
181
if {$fA==""} {
182
.txtA insert end "[lindex $line 1]\n" fn \n -
183
} else {
184
.txtA insert end "[lindex $line 1] ($fA)\n" fn \n -
185
}
186
.mkr insert end \n fn \n -
187
.lnB insert end \n fn \n -
188
if {$fB==""} {
189
.txtB insert end "[lindex $line 2]\n" fn \n -
190
} else {
191
.txtB insert end "[lindex $line 2] ($fB)\n" fn \n -
192
}
193
.wfiles.lb insert end [lindex $line 2]
194
set n1 0
195
set n2 0
196
}
197
SKIP {
198
set n [lindex $line 1]
199
incr n1 $n
200
incr n2 $n
201
.lnA insert end ...\n hrln
202
.txtA insert end [string repeat . 30]\n hrtxt
203
.mkr insert end \n hrln
204
.lnB insert end ...\n hrln
205
.txtB insert end [string repeat . 30]\n hrtxt
206
}
207
COM {
208
set x [lindex $line 1]
209
incr n1
210
incr n2
211
.lnA insert end $n1\n -
212
.txtA insert end $x\n -
213
.mkr insert end \n -
214
.lnB insert end $n2\n -
215
.txtB insert end $x\n -
216
}
217
INS {
218
set x [lindex $line 1]
219
incr n2
220
.lnA insert end \n -
221
.txtA insert end \n -
222
.mkr insert end >\n -
223
.lnB insert end $n2\n -
224
.txtB insert end $x add \n -
225
}
226
DEL {
227
set x [lindex $line 1]
228
incr n1
229
.lnA insert end $n1\n -
230
.txtA insert end $x rm \n -
231
.mkr insert end <\n -
232
.lnB insert end \n -
233
.txtB insert end \n -
234
}
235
EDIT {
236
incr n1
237
incr n2
238
.lnA insert end $n1\n -
239
.lnB insert end $n2\n -
240
.mkr insert end |\n -
241
set nn [llength $line]
242
for {set i 1} {$i<$nn} {incr i 3} {
243
set x [lindex $line $i]
244
if {$x ne ""} {
245
.txtA insert end $x -
246
.txtB insert end $x -
247
}
248
if {$i+2<$nn} {
249
set x1 [lindex $line [expr {$i+1}]]
250
set x2 [lindex $line [expr {$i+2}]]
251
if {"$x1" eq ""} {
252
.txtB insert end $x2 add
253
} elseif {"$x2" eq ""} {
254
.txtA insert end $x1 rm
255
} else {
256
.txtA insert end $x1 chng
257
.txtB insert end $x2 chng
258
}
259
}
260
}
261
.txtA insert end \n -
262
.txtB insert end \n -
263
}
264
"" {
265
foreach wx [list [string length $n1] [string length $n2]] {
266
if {$wx>$widths(ln)} {set widths(ln) $wx}
267
}
268
}
269
default {
270
.lnA insert end \n -
271
.txtA insert end $line\n err
272
.mkr insert end \n -
273
.lnB insert end \n -
274
.txtB insert end $line\n err
275
}
276
}
277
}
278
279
foreach c [cols] {
280
set type [colType $c]
281
if {$type ne "txt"} {
282
$c config -width $widths($type)
283
}
284
$c config -state disabled
285
}
286
.wfiles.lb config -height $nDiffs
287
if {$nDiffs <= [.wfiles.lb cget -height]} {
288
grid remove .wfiles.sb
289
}
290
291
return $nDiffs
292
}
293
294
proc viewDiff {idx} {
295
.txtA yview $idx
296
.txtA xview moveto 0
297
}
298
299
proc cycleDiffs {{reverse 0}} {
300
if {$reverse} {
301
set range [.txtA tag prevrange fn @0,0 1.0]
302
if {$range eq ""} {
303
viewDiff {fn.last -1c}
304
} else {
305
viewDiff [lindex $range 0]
306
}
307
} else {
308
set range [.txtA tag nextrange fn {@0,0 +1c} end]
309
if {$range eq "" || [lindex [.txtA yview] 1] == 1} {
310
viewDiff fn.first
311
} else {
312
viewDiff [lindex $range 0]
313
}
314
}
315
}
316
317
proc xvis {col} {
318
set view [$col xview]
319
return [expr {[lindex $view 1]-[lindex $view 0]}]
320
}
321
322
proc scroll-x {args} {
323
set c .txt[expr {[xvis .txtA] < [xvis .txtB] ? "A" : "B"}]
324
eval $c xview $args
325
}
326
327
interp alias {} scroll-y {} .txtA yview
328
329
proc noop {args} {}
330
331
proc enableSync {axis} {
332
update idletasks
333
interp alias {} sync-$axis {}
334
rename _sync-$axis sync-$axis
335
}
336
337
proc disableSync {axis} {
338
rename sync-$axis _sync-$axis
339
interp alias {} sync-$axis {} noop
340
}
341
342
proc sync-x {col first last} {
343
disableSync x
344
$col xview moveto [expr {$first*[xvis $col]/($last-$first)}]
345
foreach side {A B} {
346
set sb .sbx$side
347
set xview [.txt$side xview]
348
if {[lindex $xview 0] > 0 || [lindex $xview 1] < 1} {
349
grid $sb
350
eval $sb set $xview
351
} else {
352
grid remove $sb
353
}
354
}
355
enableSync x
356
}
357
358
proc sync-y {first last} {
359
disableSync y
360
foreach c [cols] {
361
$c yview moveto $first
362
}
363
if {$first > 0 || $last < 1} {
364
grid .sby
365
.sby set $first $last
366
} else {
367
grid remove .sby
368
}
369
enableSync y
370
}
371
372
wm withdraw .
373
wm title . $CFG(TITLE)
374
wm iconname . $CFG(TITLE)
375
# Keystroke bindings for on the top-level window for navigation and
376
# control also fire when those same keystrokes are pressed in the
377
# Search entry box. Disable them, to prevent the diff screen from
378
# disappearing abruptly and unexpectedly when searching for "q".
379
#
380
bind . <Control-q> exit
381
bind . <Control-p> {catch searchPrev; break}
382
bind . <Control-n> {catch searchNext; break}
383
bind . <Escape><Escape> exit
384
bind . <Destroy> {after 0 exit}
385
bind . <Tab> {cycleDiffs; break}
386
bind . <<PrevWindow>> {cycleDiffs 1; break}
387
bind . <Control-f> {searchOnOff; break}
388
bind . <Control-g> {catch searchNext; break}
389
bind . <Return> {
390
event generate .bb.files <1>
391
event generate .bb.files <ButtonRelease-1>
392
break
393
}
394
foreach {key axis args} {
395
Up y {scroll -5 units}
396
k y {scroll -5 units}
397
Down y {scroll 5 units}
398
j y {scroll 5 units}
399
Left x {scroll -5 units}
400
h x {scroll -5 units}
401
Right x {scroll 5 units}
402
l x {scroll 5 units}
403
Prior y {scroll -1 page}
404
b y {scroll -1 page}
405
Next y {scroll 1 page}
406
space y {scroll 1 page}
407
Home y {moveto 0}
408
g y {moveto 0}
409
End y {moveto 1}
410
} {
411
bind . <$key> "scroll-$axis $args; break"
412
bind . <Shift-$key> continue
413
}
414
415
frame .bb
416
::ttk::menubutton .bb.files -text "Files"
417
if {[tk windowingsystem] eq "win32"} {
418
::ttk::style theme use winnative
419
.bb.files configure -padding {20 1 10 2}
420
}
421
toplevel .wfiles
422
wm withdraw .wfiles
423
update idletasks
424
wm transient .wfiles .
425
wm overrideredirect .wfiles 1
426
listbox .wfiles.lb -width 0 -height $CFG(LB_HEIGHT) -activestyle none \
427
-yscroll {.wfiles.sb set}
428
::ttk::scrollbar .wfiles.sb -command {.wfiles.lb yview}
429
grid .wfiles.lb .wfiles.sb -sticky ns
430
bind .bb.files <1> {
431
set x [winfo rootx %W]
432
set y [expr {[winfo rooty %W]+[winfo height %W]}]
433
wm geometry .wfiles +$x+$y
434
wm deiconify .wfiles
435
focus .wfiles.lb
436
}
437
bind .wfiles <FocusOut> {wm withdraw .wfiles}
438
bind .wfiles <Escape> {focus .}
439
foreach evt {1 Return} {
440
bind .wfiles.lb <$evt> {
441
catch {
442
set idx [lindex [.txtA tag ranges fn] [expr {[%W curselection]*2}]]
443
viewDiff $idx
444
}
445
focus .
446
break
447
}
448
}
449
bind .wfiles.lb <Motion> {
450
%W selection clear 0 end
451
%W selection set @%x,%y
452
}
453
454
foreach {side syncCol} {A .txtB B .txtA} {
455
set ln .ln$side
456
text $ln
457
$ln tag config - -justify right
458
459
set txt .txt$side
460
text $txt -width $CFG(WIDTH) -height $CFG(HEIGHT) -wrap none \
461
-xscroll "sync-x $syncCol"
462
catch {$txt config -tabstyle wordprocessor} ;# Required for Tk>=8.5
463
foreach tag {add rm chng} {
464
$txt tag config $tag -background $CFG([string toupper $tag]_BG)
465
$txt tag lower $tag
466
}
467
$txt tag config fn -background $CFG(FN_BG) -foreground $CFG(FN_FG) \
468
-justify center
469
$txt tag config err -foreground $CFG(ERR_FG)
470
}
471
text .mkr
472
473
foreach c [cols] {
474
set keyPrefix [string toupper [colType $c]]_COL_
475
if {[tk windowingsystem] eq "win32"} {$c config -font {courier 9}}
476
$c config -bg $CFG(${keyPrefix}BG) -fg $CFG(${keyPrefix}FG) -borderwidth 0 \
477
-padx $CFG(PADX) -yscroll sync-y
478
$c tag config hrln -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \
479
-foreground $CFG(HR_FG) -justify right
480
$c tag config hrtxt -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \
481
-foreground $CFG(HR_FG) -justify center
482
$c tag config fn -spacing1 $CFG(FN_PAD) -spacing3 $CFG(FN_PAD)
483
bindtags $c ". $c Text all"
484
bind $c <1> {focus %W}
485
}
486
487
::ttk::scrollbar .sby -command {.txtA yview} -orient vertical
488
::ttk::scrollbar .sbxA -command {.txtA xview} -orient horizontal
489
::ttk::scrollbar .sbxB -command {.txtB xview} -orient horizontal
490
frame .spacer
491
492
if {[readDiffs $fossilcmd 0] == 0} {
493
tk_messageBox -type ok -title $CFG(TITLE) -message "No changes"
494
exit
495
}
496
update idletasks
497
498
proc saveDiff {} {
499
set fn [tk_getSaveFile]
500
if {$fn==""} return
501
set out [open $fn wb]
502
puts $out "#!/usr/bin/tclsh\n#\n# Run this script using 'tclsh' or 'wish'"
503
puts $out "# to see the graphical diff.\n#"
504
puts $out "set fossilcmd {}"
505
puts $out "set darkmode $::darkmode"
506
puts $out "set debug $::debug"
507
puts $out "set prog [list $::prog]"
508
puts $out "set difftxt \173"
509
foreach e $::difftxt {puts $out [list $e]}
510
puts $out "\175"
511
puts $out "eval \$prog"
512
close $out
513
}
514
proc invertDiff {} {
515
global CFG
516
array set x [grid info .txtA]
517
if {$x(-column)==1} {
518
grid config .lnB -column 0
519
grid config .txtB -column 1
520
.txtB tag config add -background $CFG(RM_BG)
521
grid config .lnA -column 3
522
grid config .txtA -column 4
523
.txtA tag config rm -background $CFG(ADD_BG)
524
.bb.invert config -text Uninvert
525
} else {
526
grid config .lnA -column 0
527
grid config .txtA -column 1
528
.txtA tag config rm -background $CFG(RM_BG)
529
grid config .lnB -column 3
530
grid config .txtB -column 4
531
.txtB tag config add -background $CFG(ADD_BG)
532
.bb.invert config -text Invert
533
}
534
.mkr config -state normal
535
set clt [.mkr search -all < 1.0 end]
536
set cgt [.mkr search -all > 1.0 end]
537
foreach c $clt {.mkr replace $c "$c +1 chars" >}
538
foreach c $cgt {.mkr replace $c "$c +1 chars" <}
539
.mkr config -state disabled
540
}
541
proc searchOnOff {} {
542
if {[info exists ::search]} {
543
unset ::search
544
.txtA tag remove search 1.0 end
545
.txtB tag remove search 1.0 end
546
pack forget .bb.sframe
547
focus .
548
} else {
549
set ::search .txtA
550
if {![winfo exists .bb.sframe]} {
551
frame .bb.sframe
552
::ttk::entry .bb.sframe.e -width 10
553
pack .bb.sframe.e -side left -fill y -expand 1
554
bind .bb.sframe.e <Return> {searchNext; break}
555
::ttk::button .bb.sframe.nx -text \u2193 -width 1 -command searchNext
556
::ttk::button .bb.sframe.pv -text \u2191 -width 1 -command searchPrev
557
tk_optionMenu .bb.sframe.typ ::search_type \
558
Exact {No Case} {RegExp} {Whole Word}
559
.bb.sframe.typ config -width 10
560
set ::search_type Exact
561
pack .bb.sframe.nx .bb.sframe.pv .bb.sframe.typ -side left
562
}
563
pack .bb.sframe -side left
564
after idle {focus .bb.sframe.e}
565
}
566
}
567
proc searchNext {} {searchStep -forwards +1 1.0 end}
568
proc searchPrev {} {searchStep -backwards -1 end 1.0}
569
proc searchStep {direction incr start stop} {
570
set pattern [.bb.sframe.e get]
571
if {$pattern==""} return
572
set count 0
573
set w $::search
574
if {"$w"==".txtA"} {set other .txtB} {set other .txtA}
575
if {[lsearch [$w mark names] search]<0} {
576
$w mark set search $start
577
}
578
switch $::search_type {
579
Exact {set st -exact}
580
{No Case} {set st -nocase}
581
{RegExp} {set st -regexp}
582
{Whole Word} {set st -regexp; set pattern \\y$pattern\\y}
583
}
584
set idx [$w search -count count $direction $st -- \
585
$pattern "search $incr chars" $stop]
586
if {"$idx"==""} {
587
set idx [$other search -count count $direction $st -- $pattern $start $stop]
588
if {"$idx"!=""} {
589
set this $w
590
set w $other
591
set other $this
592
} else {
593
set idx [$w search -count count $direction $st -- $pattern $start $stop]
594
}
595
}
596
$w tag remove search 1.0 end
597
$w mark unset search
598
$other tag remove search 1.0 end
599
$other mark unset search
600
if {"$idx"!=""} {
601
$w mark set search $idx
602
$w yview -pickplace $idx
603
$w tag add search search "$idx +$count chars"
604
$w tag config search -background {#fcc000}
605
}
606
set ::search $w
607
}
608
::ttk::button .bb.quit -text {Quit} -command exit
609
::ttk::button .bb.reload -text {Reload} -command reloadDiff
610
::ttk::button .bb.invert -text {Invert} -command invertDiff
611
::ttk::button .bb.save -text {Save As...} -command saveDiff
612
::ttk::button .bb.search -text {Search} -command searchOnOff
613
pack .bb.quit -side left
614
if {$fossilcmd ne ""} {
615
pack .bb.reload -side left
616
}
617
pack .bb.invert -side left
618
if {$fossilcmd!=""} {pack .bb.save -side left}
619
pack .bb.files .bb.search -side left
620
grid rowconfigure . 1 -weight 1
621
grid columnconfigure . 1 -weight 1
622
grid columnconfigure . 4 -weight 1
623
grid .bb -row 0 -columnspan 6
624
eval grid [cols] -row 1 -sticky nsew
625
grid .sby -row 1 -column 5 -sticky ns
626
grid .sbxA -row 2 -columnspan 2 -sticky ew
627
grid .spacer -row 2 -column 2
628
grid .sbxB -row 2 -column 3 -columnspan 2 -sticky ew
629
630
.spacer config -height [winfo height .sbxA]
631
wm deiconify .
632
}
633
eval $prog
634

Keyboard Shortcuts

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