Fossil SCM

The "fossil diff --tk USER@HOST:DIR" form now works. Needs better error checking for the case where --tk is omitted. Maybe other checks too. This is a work in progress.

drh 2026-08-01 23:51 UTC trunk
Commit 6bfdfc3079be7ea6f1e9944e1faa80f3388d58743665f97857244d1fa2be6ef8
2 files changed +72 -10 +41 -6
+72 -10
--- src/diff.tcl
+++ src/diff.tcl
@@ -1,11 +1,25 @@
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:
1
+# Various diff commands ("fossil diff", "fossil stash diff", "fossil fdiff",
2
+# "fossil patch diff", and others) that include the --tk option, or that use "gdiff"
3
+# instead of diff, work by running a Tcl/Tk script which is just this script prepended
4
+# with some variable initializations. The key variable is fossilcmd which is
5
+# usually set something like this:
56
#
67
# set fossilcmd {| "./fossil" diff --tcl -i -v}
8
+#
9
+# If there are additional command-line options or arguments, those will be appended
10
+# as well. This variable becomes the argument to "open" to gather the diff
11
+# information that is to be displayed.
12
+#
13
+# Other variables initializations that might be prepended to this script include:
14
+#
15
+# set debug 1
16
+# set darkmode 1
17
+# set remotehost hostname
18
+# set remotedir directory-on-hostname
19
+#
20
+# After prepending those "set" commands, the resulting script is run using "tclsh".
721
#
822
# This header comment is stripped off by the "mkbuiltin.c" program.
923
#
1024
set prog {
1125
package require Tk
@@ -94,20 +108,24 @@
94108
95109
proc reloadDiff {} {
96110
global fossilcmd difftxt
97111
unset -nocomplain difftxt
98112
set idx [.txtA index @0,0]
99
- readDiffs $fossilcmd 1
113
+ readDiffs 1
100114
update
101115
viewDiff $idx
102116
}
103117
104
-proc readDiffs {fossilcmd redo} {
105
- global difftxt debug
106
- if {![info exists difftxt]} {
118
+# Populate the difftxt global variable by running the fossilcmd command
119
+# and capturing the output. This is a reload if $redo is 1 and the initial
120
+# load if $redo is 0.
121
+#
122
+proc fetchRawDiff {redo} {
123
+ global fossilcmd remotehost remotedir debug difftxt
124
+ if {![info exists remotehost] || $remotehost eq ""} {
107125
if {$debug} {
108
- puts "# [list open $fossilcmd r]"
126
+ puts "# [lrange $fossilcmd 1 end]"
109127
flush stdout
110128
}
111129
if {[catch {
112130
set in [open $fossilcmd r]
113131
fconfigure $in -encoding utf-8
@@ -120,10 +138,54 @@
120138
} else {
121139
puts $msg
122140
exit 1
123141
}
124142
}
143
+ return 0
144
+ } else {
145
+ set suffix [lrange $fossilcmd 2 end]
146
+ set cmd "| ssh $remotehost {cd $remotedir && fossil $suffix}"
147
+ if {$debug} {
148
+ puts "# [lrange $cmd 1 end]"
149
+ flush stdout
150
+ }
151
+ if {![catch {
152
+ set in [open $cmd r]
153
+ fconfigure $in -encoding utf-8
154
+ set difftxt [split [read $in] \n]
155
+ close $in
156
+ }]} {
157
+ return 1
158
+ }
159
+ set cmd "| ssh $remotehost {cd $remotedir && \
160
+ PATH=\$HOME/bin:/usr/local/bin:/opt/homebrew/bin:\$PATH fossil $suffix}"
161
+ if {$debug} {
162
+ puts "# [lrange $cmd 1 end]"
163
+ flush stdout
164
+ }
165
+ if {![catch {
166
+ set in [open $cmd r]
167
+ fconfigure $in -encoding utf-8
168
+ set difftxt [split [read $in] \n]
169
+ close $in
170
+ } msg]} {
171
+ return 1
172
+ }
173
+ if {$redo} {
174
+ tk_messageBox -type ok -title Error -message "Unable to refresh:\n$msg"
175
+ return 0
176
+ } else {
177
+ puts $msg
178
+ exit 1
179
+ }
180
+ }
181
+}
182
+
183
+proc readDiffs {redo} {
184
+ global fossilcmd difftxt debug
185
+ if {![info exists difftxt]} {
186
+ fetchRawDiff $redo
125187
}
126188
set N [llength $difftxt]
127189
set ii 0
128190
set nDiffs 0
129191
set n1 0
@@ -487,11 +549,11 @@
487549
::ttk::scrollbar .sby -command {.txtA yview} -orient vertical
488550
::ttk::scrollbar .sbxA -command {.txtA xview} -orient horizontal
489551
::ttk::scrollbar .sbxB -command {.txtB xview} -orient horizontal
490552
frame .spacer
491553
492
-if {[readDiffs $fossilcmd 0] == 0} {
554
+if {[readDiffs 0] == 0} {
493555
tk_messageBox -type ok -title $CFG(TITLE) -message "No changes"
494556
exit
495557
}
496558
update idletasks
497559
498560
--- src/diff.tcl
+++ src/diff.tcl
@@ -1,11 +1,25 @@
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
@@ -94,20 +108,24 @@
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
@@ -120,10 +138,54 @@
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
@@ -487,11 +549,11 @@
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
--- src/diff.tcl
+++ src/diff.tcl
@@ -1,11 +1,25 @@
1 # Various diff commands ("fossil diff", "fossil stash diff", "fossil fdiff",
2 # "fossil patch diff", and others) that include the --tk option, or that use "gdiff"
3 # instead of diff, work by running a Tcl/Tk script which is just this script prepended
4 # with some variable initializations. The key variable is fossilcmd which is
5 # usually set something like this:
6 #
7 # set fossilcmd {| "./fossil" diff --tcl -i -v}
8 #
9 # If there are additional command-line options or arguments, those will be appended
10 # as well. This variable becomes the argument to "open" to gather the diff
11 # information that is to be displayed.
12 #
13 # Other variables initializations that might be prepended to this script include:
14 #
15 # set debug 1
16 # set darkmode 1
17 # set remotehost hostname
18 # set remotedir directory-on-hostname
19 #
20 # After prepending those "set" commands, the resulting script is run using "tclsh".
21 #
22 # This header comment is stripped off by the "mkbuiltin.c" program.
23 #
24 set prog {
25 package require Tk
@@ -94,20 +108,24 @@
108
109 proc reloadDiff {} {
110 global fossilcmd difftxt
111 unset -nocomplain difftxt
112 set idx [.txtA index @0,0]
113 readDiffs 1
114 update
115 viewDiff $idx
116 }
117
118 # Populate the difftxt global variable by running the fossilcmd command
119 # and capturing the output. This is a reload if $redo is 1 and the initial
120 # load if $redo is 0.
121 #
122 proc fetchRawDiff {redo} {
123 global fossilcmd remotehost remotedir debug difftxt
124 if {![info exists remotehost] || $remotehost eq ""} {
125 if {$debug} {
126 puts "# [lrange $fossilcmd 1 end]"
127 flush stdout
128 }
129 if {[catch {
130 set in [open $fossilcmd r]
131 fconfigure $in -encoding utf-8
@@ -120,10 +138,54 @@
138 } else {
139 puts $msg
140 exit 1
141 }
142 }
143 return 0
144 } else {
145 set suffix [lrange $fossilcmd 2 end]
146 set cmd "| ssh $remotehost {cd $remotedir && fossil $suffix}"
147 if {$debug} {
148 puts "# [lrange $cmd 1 end]"
149 flush stdout
150 }
151 if {![catch {
152 set in [open $cmd r]
153 fconfigure $in -encoding utf-8
154 set difftxt [split [read $in] \n]
155 close $in
156 }]} {
157 return 1
158 }
159 set cmd "| ssh $remotehost {cd $remotedir && \
160 PATH=\$HOME/bin:/usr/local/bin:/opt/homebrew/bin:\$PATH fossil $suffix}"
161 if {$debug} {
162 puts "# [lrange $cmd 1 end]"
163 flush stdout
164 }
165 if {![catch {
166 set in [open $cmd r]
167 fconfigure $in -encoding utf-8
168 set difftxt [split [read $in] \n]
169 close $in
170 } msg]} {
171 return 1
172 }
173 if {$redo} {
174 tk_messageBox -type ok -title Error -message "Unable to refresh:\n$msg"
175 return 0
176 } else {
177 puts $msg
178 exit 1
179 }
180 }
181 }
182
183 proc readDiffs {redo} {
184 global fossilcmd difftxt debug
185 if {![info exists difftxt]} {
186 fetchRawDiff $redo
187 }
188 set N [llength $difftxt]
189 set ii 0
190 set nDiffs 0
191 set n1 0
@@ -487,11 +549,11 @@
549 ::ttk::scrollbar .sby -command {.txtA yview} -orient vertical
550 ::ttk::scrollbar .sbxA -command {.txtA xview} -orient horizontal
551 ::ttk::scrollbar .sbxB -command {.txtB xview} -orient horizontal
552 frame .spacer
553
554 if {[readDiffs 0] == 0} {
555 tk_messageBox -type ok -title $CFG(TITLE) -message "No changes"
556 exit
557 }
558 update idletasks
559
560
+41 -6
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -1187,10 +1187,35 @@
11871187
){
11881188
return 1;
11891189
}
11901190
return 0;
11911191
}
1192
+
1193
+/*
1194
+** A TCL list is being constructed in pBlob. Append a single list
1195
+** element z[] to that list. Do all necessary quoting and escaping.
1196
+** The content of z[] might be controlled by an attacker.
1197
+*/
1198
+static void diff_tcl_lappend(Blob *pBlob, const char *z, int isFilename){
1199
+ if( strchr(z,'{')==0 && strchr(z,'}')==0 ){
1200
+ if( isFilename ){
1201
+ blob_appendf(pBlob, " {%/}", z);
1202
+ }else{
1203
+ blob_appendf(pBlob, " {%s}", z);
1204
+ }
1205
+ }else{
1206
+ int j;
1207
+ blob_append_char(pBlob, ' ');
1208
+ for(j=0; z[j]; j++){
1209
+ if( fossil_isalnum(z[j]) ){
1210
+ blob_append_char(pBlob, z[j]);
1211
+ }else{
1212
+ blob_appendf(pBlob, "\\%03o", (unsigned char)z[j]);
1213
+ }
1214
+ }
1215
+ }
1216
+}
11921217
11931218
/*
11941219
** Show diff output in a Tcl/Tk window, in response to the --tk option
11951220
** to the diff command.
11961221
**
@@ -1205,10 +1230,12 @@
12051230
int i;
12061231
Blob script;
12071232
const char *zTempFile = 0;
12081233
char *zCmd;
12091234
const char *zTclsh;
1235
+ const char *zHost = 0;
1236
+ const char *zDir = 0;
12101237
int bDebug = find_option("tkdebug",0,0)!=0;
12111238
int bDarkMode = find_option("dark",0,0)!=0;
12121239
(void)find_option("debug",0,0);
12131240
blob_zero(&script);
12141241
/* Caution: When this routine is called from the merge-info command,
@@ -1232,20 +1259,28 @@
12321259
** be written into the FILENAME instead of being run. This is used
12331260
** for testing and debugging. */
12341261
zTempFile = find_option("script",0,1);
12351262
for(i=firstArg; i<g.argc; i++){
12361263
const char *z = g.argv[i];
1237
- if( sqlite3_strglob("*}*",z) ){
1238
- blob_appendf(&script, " {%/}", z);
1239
- }else{
1240
- int j;
1241
- blob_append(&script, " ", 1);
1242
- for(j=0; z[j]; j++) blob_appendf(&script, "\\%03o", (unsigned char)z[j]);
1264
+ if( zHost==0
1265
+ && !file_isfile_or_link(z)
1266
+ && (zDir = file_skip_userhost(z))!=0
1267
+ ){
1268
+ zHost = mprintf("%.*s", (int)(zDir - z - 1), z);
1269
+ continue;
12431270
}
1271
+ diff_tcl_lappend(&script, z, 1);
12441272
}
12451273
blob_appendf(&script, "}\nset darkmode %d\n", bDarkMode);
12461274
blob_appendf(&script, "set debug %d\n", bDebug);
1275
+ if( zHost ){
1276
+ blob_appendf(&script, "set remotehost");
1277
+ diff_tcl_lappend(&script, zHost, 0);
1278
+ blob_appendf(&script, "\nset remotedir");
1279
+ diff_tcl_lappend(&script, zDir, 1);
1280
+ blob_append_char(&script, '\n');
1281
+ }
12471282
blob_appendf(&script, "%s", builtin_file("diff.tcl", 0));
12481283
if( zTempFile ){
12491284
blob_write_to_file(&script, zTempFile);
12501285
fossil_print("To see diff, run: %s \"%s\"\n", zTclsh, zTempFile);
12511286
}else{
12521287
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -1187,10 +1187,35 @@
1187 ){
1188 return 1;
1189 }
1190 return 0;
1191 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
1193 /*
1194 ** Show diff output in a Tcl/Tk window, in response to the --tk option
1195 ** to the diff command.
1196 **
@@ -1205,10 +1230,12 @@
1205 int i;
1206 Blob script;
1207 const char *zTempFile = 0;
1208 char *zCmd;
1209 const char *zTclsh;
 
 
1210 int bDebug = find_option("tkdebug",0,0)!=0;
1211 int bDarkMode = find_option("dark",0,0)!=0;
1212 (void)find_option("debug",0,0);
1213 blob_zero(&script);
1214 /* Caution: When this routine is called from the merge-info command,
@@ -1232,20 +1259,28 @@
1232 ** be written into the FILENAME instead of being run. This is used
1233 ** for testing and debugging. */
1234 zTempFile = find_option("script",0,1);
1235 for(i=firstArg; i<g.argc; i++){
1236 const char *z = g.argv[i];
1237 if( sqlite3_strglob("*}*",z) ){
1238 blob_appendf(&script, " {%/}", z);
1239 }else{
1240 int j;
1241 blob_append(&script, " ", 1);
1242 for(j=0; z[j]; j++) blob_appendf(&script, "\\%03o", (unsigned char)z[j]);
1243 }
 
1244 }
1245 blob_appendf(&script, "}\nset darkmode %d\n", bDarkMode);
1246 blob_appendf(&script, "set debug %d\n", bDebug);
 
 
 
 
 
 
 
1247 blob_appendf(&script, "%s", builtin_file("diff.tcl", 0));
1248 if( zTempFile ){
1249 blob_write_to_file(&script, zTempFile);
1250 fossil_print("To see diff, run: %s \"%s\"\n", zTclsh, zTempFile);
1251 }else{
1252
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -1187,10 +1187,35 @@
1187 ){
1188 return 1;
1189 }
1190 return 0;
1191 }
1192
1193 /*
1194 ** A TCL list is being constructed in pBlob. Append a single list
1195 ** element z[] to that list. Do all necessary quoting and escaping.
1196 ** The content of z[] might be controlled by an attacker.
1197 */
1198 static void diff_tcl_lappend(Blob *pBlob, const char *z, int isFilename){
1199 if( strchr(z,'{')==0 && strchr(z,'}')==0 ){
1200 if( isFilename ){
1201 blob_appendf(pBlob, " {%/}", z);
1202 }else{
1203 blob_appendf(pBlob, " {%s}", z);
1204 }
1205 }else{
1206 int j;
1207 blob_append_char(pBlob, ' ');
1208 for(j=0; z[j]; j++){
1209 if( fossil_isalnum(z[j]) ){
1210 blob_append_char(pBlob, z[j]);
1211 }else{
1212 blob_appendf(pBlob, "\\%03o", (unsigned char)z[j]);
1213 }
1214 }
1215 }
1216 }
1217
1218 /*
1219 ** Show diff output in a Tcl/Tk window, in response to the --tk option
1220 ** to the diff command.
1221 **
@@ -1205,10 +1230,12 @@
1230 int i;
1231 Blob script;
1232 const char *zTempFile = 0;
1233 char *zCmd;
1234 const char *zTclsh;
1235 const char *zHost = 0;
1236 const char *zDir = 0;
1237 int bDebug = find_option("tkdebug",0,0)!=0;
1238 int bDarkMode = find_option("dark",0,0)!=0;
1239 (void)find_option("debug",0,0);
1240 blob_zero(&script);
1241 /* Caution: When this routine is called from the merge-info command,
@@ -1232,20 +1259,28 @@
1259 ** be written into the FILENAME instead of being run. This is used
1260 ** for testing and debugging. */
1261 zTempFile = find_option("script",0,1);
1262 for(i=firstArg; i<g.argc; i++){
1263 const char *z = g.argv[i];
1264 if( zHost==0
1265 && !file_isfile_or_link(z)
1266 && (zDir = file_skip_userhost(z))!=0
1267 ){
1268 zHost = mprintf("%.*s", (int)(zDir - z - 1), z);
1269 continue;
1270 }
1271 diff_tcl_lappend(&script, z, 1);
1272 }
1273 blob_appendf(&script, "}\nset darkmode %d\n", bDarkMode);
1274 blob_appendf(&script, "set debug %d\n", bDebug);
1275 if( zHost ){
1276 blob_appendf(&script, "set remotehost");
1277 diff_tcl_lappend(&script, zHost, 0);
1278 blob_appendf(&script, "\nset remotedir");
1279 diff_tcl_lappend(&script, zDir, 1);
1280 blob_append_char(&script, '\n');
1281 }
1282 blob_appendf(&script, "%s", builtin_file("diff.tcl", 0));
1283 if( zTempFile ){
1284 blob_write_to_file(&script, zTempFile);
1285 fossil_print("To see diff, run: %s \"%s\"\n", zTclsh, zTempFile);
1286 }else{
1287

Keyboard Shortcuts

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