git-gui: add build tab
[git-gui/bertw.git] / lib / build.tcl
blob1ae1eb4bd0b0af961069089c9766fdbea6fcbf62
1 class build {
3 # widgets
4 field w
5 field w_hpane
6 field w_vpane
8 field w_files
9 field w_diags
10 field w_errs
11 field ui_errs_width 3
12 field ui_errs_width_max
13 field w_warns
14 field ui_warns_width 3
15 field ui_warns_width_max
16 field ui_files_cols
18 field w_lnos
19 field ui_lnos_width 5
20 field ui_lnos_width_max
21 field w_hits
22 field ui_hits_cols
24 field w_indicator
25 field m_configs
26 field w_output
27 field ui_finder
28 field w_entry
30 field state
31 field state_change_cb
33 field file_list
34 field current_file_list
35 field file_info
36 field diag_list
37 field current_diag
38 field current_path {}
39 field current_path_label {}
40 field current_path_lno_hits
41 field file_list_needs_update 0
42 field busy 0
43 field file_list_busy 0
44 field hits_busy 0
45 field current_fd
46 field vpath {.}
47 field dir_stack {}
48 field run_nl {}
49 field hits_nl {}
51 field shell
52 field envmods
53 field configmods
54 field selected_configs
55 field buildconfig_config
56 field buildconfig_menu_id 0
58 # record and history
59 field current_cmd {}
60 field current_cmd_label {}
61 field build_ref refs/builds/default
62 field build_name
63 field build_index
64 field output_hash_pipe
65 field output_hash_out
66 # static tree entries (cwd, environ)
67 field static_build_tree
68 # per run tree entries (output, exit status, worktree)
69 field build_tree
70 field build_start
71 field build_start_s
72 field build_end
73 field build_run_s 0
74 field build_timer
75 field cmd_history
76 field cmd_history_pos 0
77 field build_history
78 field build_history_pos -1
80 constructor embed {i_w {i_vpath {}} {i_ref {}} {i_shell {}} {i_envmods {}} {i_configs {}} {i_state_change_cb {}}} {
82 set w $i_w
84 if {[catch {_init $this $i_vpath $i_ref $i_shell $i_envmods $i_configs $i_state_change_cb} err]} {
85 return -code error $err
88 return $this
91 method _init {i_vpath i_ref i_shell i_envmods i_configs i_state_change_cb} {
92 global env
94 if {$i_vpath ne {}} {
95 set vpath $i_vpath
98 if {$i_ref ne {}} {
99 switch -glob $i_ref {
101 {return -code error "Invalid build ref ending in /: $i_ref"}
102 refs/builds/*
103 {set build_ref $i_ref}
104 refs/*
105 {return -code error "Invalid build ref: $i_ref"}
106 builds/*
107 {set build_ref refs/$i_ref}
109 {set build_ref refs/builds/$i_ref}
112 set build_name [string range $build_ref [string length {refs/}] end]
113 set build_index $::GIT_INDEX_FILE.[string map {/ .} $build_name].[pid]
115 if {$i_shell eq {}} {
116 set shell [shellpath]
117 } else {
118 set shell $i_shell
121 set envmods [list]
122 foreach mod $i_envmods {
123 if {[regexp {^([A-Za-z0-9_]+)\s*((?:[-+%].?)?=)\s*(.*)$} $mod match name op value]} {
124 lappend envmods $name $op $value
125 } elseif {[regexp {^!([A-Za-z0-9_]+)$} $mod match name]} {
126 lappend envmods $name ! {}
127 } else {
128 tk_messageBox \
129 -icon warning \
130 -type ok \
131 -title {git-gui: build: invalid env mod} \
132 -message "Invalid env mod command: $mod"
136 set configmods $i_configs
137 array set selected_configs {}
139 set state_change_cb $i_state_change_cb
141 # base path for all grep widgets
142 set w_hpane $w.h
143 set w_vpane $w_hpane.v
145 set w_files $w_vpane.f.l
146 set w_diags $w_vpane.f.t.k
147 set w_errs $w_vpane.f.e
148 set w_warns $w_vpane.f.w
150 set w_lnos $w_vpane.o.l
151 set w_hits $w_vpane.o.d
153 set w_indicator $w_hpane.f.l
154 set m_configs $w_hpane.f.l.c
155 set w_output $w_hpane.f.o
156 set w_entry $w_hpane.f.e
158 ttk::panedwindow $w_hpane -orient vertical
159 ttk::panedwindow $w_vpane -orient horizontal
161 ttk::frame $w_hpane.f -borderwidth 0
162 pack $w_hpane -side top -fill both -expand 1
164 $w_hpane add $w_vpane -weight 0
165 $w_hpane add $w_hpane.f -weight 1
167 ttk::frame $w_vpane.f -borderwidth 0
168 ttk::frame $w_vpane.o -borderwidth 0
169 $w_vpane add $w_vpane.f -weight 0
170 $w_vpane add $w_vpane.o -weight 1
172 ## list of files with errors and warnings
174 ttk::frame $w_vpane.f.t -borderwidth 0
176 ttk::label $w_vpane.f.t.l \
177 -style Color.TLabel \
178 -text "Diagnosed Files" \
179 -background lightsalmon \
180 -foreground black
182 set diag_list [list "All"]
184 ttk::combobox $w_diags \
185 -style Color.TCombobox \
186 -state readonly \
187 -takefocus 0 \
188 -justify right \
189 -exportselection false \
190 -textvariable push_remote \
191 -background lightsalmon \
192 -values $diag_list
194 grid configure $w_vpane.f.t.l $w_diags \
195 -sticky nsew
197 grid columnconfigure $w_vpane.f.t \
199 -weight 1
201 text $w_files \
202 -background white \
203 -foreground black \
204 -borderwidth 0 \
205 -takefocus 0 \
206 -highlightthickness 0 \
207 -padx 0 -pady 0 \
208 -state disabled \
209 -wrap none \
210 -width 20 \
211 -height 10 \
212 -xscrollcommand [list $w_vpane.f.sbx set]
213 $w_files tag conf default -lmargin1 5 -rmargin 1
215 text $w_errs \
216 -takefocus 0 \
217 -highlightthickness 0 \
218 -padx 0 -pady 0 \
219 -background grey95 \
220 -foreground black \
221 -borderwidth 0 \
222 -width [expr $ui_errs_width + 1] \
223 -height 10 \
224 -wrap none \
225 -state disabled
226 $w_errs tag conf count -justify right -lmargin1 2 -rmargin 3 -foreground red
228 text $w_warns \
229 -takefocus 0 \
230 -highlightthickness 0 \
231 -padx 0 -pady 0 \
232 -background grey90 \
233 -foreground black \
234 -borderwidth 0 \
235 -width [expr $ui_warns_width + 1] \
236 -height 10 \
237 -wrap none \
238 -state disabled
239 $w_warns tag conf count -justify right -lmargin1 2 -rmargin 3 -foreground orange
241 set ui_files_cols [list $w_files $w_errs $w_warns]
243 # simulate linespacing, as if it has an icon like the index/worktree
244 # lists
245 set fn [$w_files cget -font]
246 set ls [font metrics $fn -linespace]
247 if {$ls < 17} {
248 set d [expr 17 - $ls]
249 set b [expr $d / 2]
250 set t $b
251 if {[expr $b + $t] != $d} {
252 incr b
254 foreach i $ui_files_cols {
255 $i configure -spacing1 $t -spacing3 $b
259 ttk::scrollbar $w_vpane.f.sbx \
260 -orient h \
261 -command [list $w_files xview]
263 ttk::scrollbar $w_vpane.f.sby \
264 -orient v \
265 -command [list scrollbar2many $ui_files_cols yview]
267 grid configure $w_vpane.f.t \
268 -column 0 \
269 -columnspan 4 \
270 -sticky we
272 grid $w_files $w_errs $w_warns $w_vpane.f.sby -sticky nsew
274 grid configure $w_vpane.f.sbx \
275 -column 0 \
276 -columnspan 4 \
277 -sticky we
279 grid columnconfigure $w_vpane.f \
281 -weight 1
282 grid rowconfigure $w_vpane.f \
284 -weight 1
286 foreach i $ui_files_cols {
287 rmsel_tag $i
289 $i conf -cursor arrow
290 $i conf -yscrollcommand \
291 "[list many2scrollbar $ui_files_cols yview $w_vpane.f.sby]"
293 bind $i <Button-1> "[cb _select_from_file_list %x %y]; break"
296 ## error/warning description from one file
298 ttk::label $w_vpane.o.t \
299 -style Color.TLabel \
300 -textvariable @current_path_label \
301 -background gold \
302 -foreground black \
303 -justify right \
304 -anchor e
306 set ctxm $w_vpane.o.t.ctxm
307 menu $ctxm -tearoff 0
308 $ctxm add command \
309 -label [mc Copy] \
310 -command [cb _copy_path]
311 bind_button3 $w_vpane.o.t "tk_popup $ctxm %X %Y"
313 text $w_lnos \
314 -takefocus 0 \
315 -highlightthickness 0 \
316 -padx 0 -pady 0 \
317 -background grey95 \
318 -foreground black \
319 -borderwidth 0 \
320 -width [expr $ui_lnos_width + 1] \
321 -height 10 \
322 -wrap none \
323 -state disabled \
324 -font font_diff
325 $w_lnos tag conf normal -justify right -rmargin 5
326 $w_lnos tag conf warning -justify right -rmargin 5 -foreground orange
327 $w_lnos tag conf error -justify right -rmargin 5 -foreground red
329 text $w_hits \
330 -takefocus 0 \
331 -highlightthickness 0 \
332 -padx 0 -pady 0 \
333 -background white \
334 -foreground black \
335 -borderwidth 0 \
336 -width 80 \
337 -height 10 \
338 -wrap none \
339 -xscrollcommand [list $w_vpane.o.sbx set] \
340 -state disabled \
341 -font font_diff
342 $w_hits tag conf normal
343 $w_hits tag conf warning -foreground orange
344 $w_hits tag conf error -foreground red
345 $w_hits tag conf jumpmark -elide 1
346 $w_hits tag conf auxmark -elide 1
348 delegate_sel_to $w_hits [list $w_lnos]
350 set ui_hits_cols [list $w_lnos $w_hits]
352 ttk::scrollbar $w_vpane.o.sbx \
353 -orient h \
354 -command [list $w_hits xview]
356 ttk::scrollbar $w_vpane.o.sby \
357 -orient v \
358 -command [list scrollbar2many $ui_hits_cols yview]
360 grid configure $w_vpane.o.t \
361 -column 0 \
362 -columnspan 3 \
363 -sticky we
365 grid $w_lnos $w_hits $w_vpane.o.sby -sticky nsew
367 grid configure $w_vpane.o.sbx \
368 -column 0 \
369 -columnspan 3 \
370 -sticky we
372 grid columnconfigure $w_vpane.o \
374 -weight 1
375 grid rowconfigure $w_vpane.o \
377 -weight 1
379 rmsel_tag $w_lnos
380 foreach i $ui_hits_cols {
381 $i tag raise sel
383 $i conf -yscrollcommand \
384 "[list many2scrollbar $ui_hits_cols yview $w_vpane.o.sby]"
386 set bind_cmd bind
387 if {$i ne $w_hits} {
388 set bind_cmd delegator_bind
390 $bind_cmd $i <Button-1> "[cb _jump_to_hit_in_output %x %y]"
391 bind $i <ButtonRelease-2> "[cb _open_from_hits %x %y]; break"
394 ## command output and entry
396 ttk::label $w_indicator \
397 -style Color.TLabel \
398 -textvariable @current_cmd_label \
399 -background blue \
400 -foreground white
402 menu $m_configs -tearoff 0
403 bind_button3 $w_indicator "[cb _popup_configs %X %Y]"
405 text $w_output \
406 -takefocus 0 \
407 -highlightthickness 0 \
408 -padx 0 \
409 -pady 0 \
410 -background white \
411 -foreground black \
412 -borderwidth 0 \
413 -width 80 \
414 -height 20 \
415 -wrap none \
416 -xscrollcommand [list $w_hpane.f.sbx set] \
417 -state disabled \
418 -font font_diff
419 $w_output tag conf out
420 $w_output tag conf note -foreground blue
421 $w_output tag conf warning -foreground orange
422 $w_output tag conf error -foreground red
423 #$w_output tag conf path -underline 1
424 #$w_output tag conf pos -underline 1
425 $w_output tag conf path -font font_diffitalic
426 $w_output tag conf pos -font font_diffitalic
427 $w_output tag conf found -background yellow
428 $w_output tag conf currenthit -font font_diffbold
430 foreach {n c} {0 black 1 red 2 green4 3 yellow4 4 blue4 5 magenta4 6 cyan4 7 grey60} {
431 $w_output tag configure clr4$n -background $c
432 $w_output tag configure clri4$n -foreground $c
433 $w_output tag configure clr3$n -foreground $c
434 $w_output tag configure clri3$n -background $c
436 $w_output tag configure clr1 -font font_diffbold
437 $w_output tag configure clr4 -underline 1
438 $w_output tag raise found
439 $w_output tag raise sel
441 ttk::scrollbar $w_hpane.f.sbx \
442 -orient h \
443 -command [list $w_output xview]
445 ttk::scrollbar $w_hpane.f.sby \
446 -orient v \
447 -command [list $w_output yview]
449 entry $w_entry \
450 -font TkDefaultFont \
451 -disabledforeground white \
452 -disabledbackground blue \
453 -takefocus [cb _always_takefocus]
455 grid configure $w_indicator \
456 -column 0 \
457 -columnspan 2 \
458 -sticky we
459 grid $w_output $w_hpane.f.sby -sticky nsew
460 grid configure $w_hpane.f.sbx \
461 -column 0 \
462 -columnspan 2 \
463 -sticky we
465 set ui_finder [::searchbar::new \
466 $w_hpane.f.f $w_output $w_entry \
467 -column 0 \
468 -columnspan 2 \
471 $w_output conf \
472 -yscrollcommand \
473 "[list $ui_finder scrolled]
474 [list $w_hpane.f.sby set]"
476 grid configure $w_entry \
477 -column 0 \
478 -columnspan 2 \
479 -sticky we
481 grid columnconfigure $w_hpane.f \
483 -weight 1
484 grid rowconfigure $w_hpane.f \
486 -weight 1
488 bind $w_output <ButtonRelease-2> [cb _open_from_output %x %y]
490 foreach i [list $w_output $w_entry [$ui_finder editor]] {
491 bind $i <F7> [list $ui_finder show]
492 bind $i <$::M1B-Key-f> [list $ui_finder show]
493 bind $i <Escape> [list $ui_finder hide]
494 bind $i <F3> [list $ui_finder find_next]
495 bind $i <Shift-F3> [list $ui_finder find_prev]
497 bind $i <Alt-Up> "$w_output yview scroll -1 units; break"
498 bind $i <Alt-Down> "$w_output yview scroll 1 units; break"
499 bind $i <Alt-Prior> "$w_output yview scroll -1 pages; break"
500 bind $i <Alt-Next> "$w_output yview scroll 1 pages; break"
502 # scoll of file list
503 bind $i <Alt-Shift-Up> "[cb _files_scroll_line -1]; break"
504 bind $i <Alt-Shift-Down> "[cb _files_scroll_line 1]; break"
505 bind $i <Alt-Shift-Prior> "[cb _files_scroll_page -1]; break"
506 bind $i <Alt-Shift-Next> "[cb _files_scroll_page 1]; break"
509 bind $w_entry <Return> [cb _run]
510 bind $w_entry <KP_Enter> [cb _run]
511 bind $w_entry <Key-Up> [cb _prev_cmd]
512 bind $w_entry <Key-Down> [cb _next_cmd]
513 bind $w_entry <$::M1B-Key-Up> [cb _prev_build]
514 bind $w_entry <$::M1B-Key-Down> [cb _next_build]
515 bind $w_entry <Key-Prior> [cb _search_prev_cmd]
516 bind $w_entry <Key-Next> [cb _search_next_cmd]
517 bind $w_entry <$::M1B-Key-c> [cb _cancel]
518 bind $w_entry <Visibility> [cb _visible]
520 array set current_path_lno_hits {}
521 trace add variable current_path write [cb _update_path_label]
522 set current_path {}
524 set state running
525 trace add variable state write [cb _update_cmd_label]
526 trace add variable current_cmd write [cb _update_cmd_label]
527 set current_cmd {}
529 set file_list [list]
530 set current_file_list [list]
531 array set file_info {}
532 _reset_diag_list $this
533 bind $w_diags <<ComboboxSelected>> [cb _select_diagnostics]
534 array set build_tree {}
535 array set static_build_tree {}
537 # resolve vpath
538 set vpath [file normalize [file join $::GIT_WORK_TREE $vpath]]
540 # make the vpath relative to gitwork_dir, when this is an ancestor
541 set cmd_dir $vpath
542 if {[string first "$::GIT_WORK_TREE/" $cmd_dir] == 0} {
543 set cmd_dir [string range $cmd_dir [string length "$::GIT_WORK_TREE/"]+1 end]
545 if {$cmd_dir eq $::GIT_WORK_TREE} {
546 set cmd_dir .
548 set static_build_tree(cwd) [list \
549 120000 \
550 blob \
551 [git hash-object -w -t blob --stdin <<$cmd_dir]]
553 # load history
554 set cmd_history [list ""]
555 set build_history [list]
556 catch {
557 set logfd [git_read log -g {--pretty=set hist_entry [list %H %T %at %ct {%s}]} $build_ref]
558 while {[gets $logfd entry] >= 0} {
559 eval $entry
560 foreach {build_c build_t build_start_s build_end_s cmd} $hist_entry break
561 set hist_entry [list $build_c $build_t [expr $build_end_s - $build_start_s] $cmd]
562 lappend build_history $hist_entry
563 set cmd [lindex $hist_entry 3]
564 if {[lindex $cmd_history end] eq $cmd} {
565 continue
567 lappend cmd_history $cmd
568 unset hist_entry
570 close $logfd
571 } err_info
574 method _run {} {
575 global env
577 if {$busy} return
578 set busy 1
580 if {[string trim [$w_entry get]] eq {}} {
581 set busy 0
582 return
584 set current_cmd [$w_entry get]
586 set build_history_pos -1
588 set run_nl {}
590 set state running
592 set file_list [list]
593 set current_file_list [list]
594 array unset file_info
595 set diag_list [list "All" "Errors" "Warnings"]
596 set current_diag 0
597 set current_path {}
598 array set build_tree {}
600 foreach i $ui_files_cols {
601 $i conf -state normal
602 $i delete 0.0 end
603 $i conf -state disabled
605 $w_errs conf -width [expr $ui_errs_width + 1]
606 set ui_errs_width_max $ui_errs_width
607 $w_warns conf -width [expr $ui_warns_width + 1]
608 set ui_warns_width_max $ui_warns_width
610 foreach i $ui_hits_cols {
611 $i conf -state normal
612 $i delete 0.0 end
613 $i conf -state disabled
615 $w_lnos conf -width [expr $ui_lnos_width + 1]
616 set ui_lnos_width_max $ui_lnos_width
618 $w_output conf -state normal
619 $w_output delete 0.0 end
620 $w_output conf -state disabled
621 $w_output tag remove found 1.0 end
623 $w_errs conf -width [expr $ui_errs_width + 1]
624 set ui_errs_width_max $ui_warns_width
625 $w_warns conf -width [expr $ui_warns_width + 1]
626 set ui_warns_width_max $ui_warns_width
628 # record state of work tree
629 if {[catch {
630 exec git read-tree --index-output=$build_index --reset HEAD
631 set ::GIT_INDEX_FILE $build_index
632 exec git ls-files --exclude-standard -d -m -o -z | \
633 git update-index -z --add --remove --stdin
634 set build_tree(worktree) [list \
635 040000 \
636 tree \
637 [git write-tree]]
638 file delete $build_index
639 git_reset_env
640 } err]} {
641 set state failed
642 set busy 0
643 file delete $build_index
644 git_reset_env
646 tk_messageBox \
647 -icon error \
648 -type ok \
649 -title {git-gui: build: can't record state of worktree} \
650 -message $err
652 return
655 set dir_stack [list $vpath]
656 set env_args [envargs [_get_envmods $this]]
658 # environ
659 if {[catch {
660 # open pipe to git-hash-object
661 set e_pipe [open "|cat" r+]
662 fconfigure $e_pipe \
663 -translation binary
664 set e_out [git_write hash-object -t blob -w --stdin >@$e_pipe]
665 fconfigure $e_out \
666 -translation binary
667 set cmd [concat env $env_args [list $shell -c "cd $vpath && env -0" >@$e_out]]
668 eval exec $cmd
669 close $e_out
670 set build_tree(environ) [list \
671 100644 \
672 blob \
673 [gets $e_pipe]]
674 close $e_pipe
675 } err]} {
676 catch {close $e_out}
677 catch {close $e_pipe}
678 set state failed
679 set busy 0
681 tk_messageBox \
682 -icon error \
683 -type ok \
684 -title {git-gui: build: can't record environment} \
685 -message $err
687 return
690 # open pipe to git-hash-object
691 set output_hash_pipe [open "|cat" r+]
692 fconfigure $output_hash_pipe \
693 -translation binary
694 set output_hash_out [git_write hash-object -t blob -w --stdin >@$output_hash_pipe]
695 fconfigure $output_hash_out \
696 -translation binary
698 set build_start [exec date -R]
699 set build_start_s [exec date +%s -d $build_start]
700 set build_run_s 0
701 set build_timer [after 250 [cb _update_runtime]]
702 set cmd [concat [list | env] $env_args [list $shell -c "cd $vpath && $current_cmd" 2>@1]]
703 if {[catch {set current_fd [open $cmd r]} err]} {
704 set state failed
705 set busy 0
706 close $output_hash_pipe
707 close $output_hash_out
709 tk_messageBox \
710 -icon error \
711 -type ok \
712 -title {git-gui: build: fatal error} \
713 -message $err
715 return
718 fconfigure $current_fd \
719 -blocking 0 \
720 -translation lf
721 fileevent $current_fd readable [cb _read]
724 method _load {} {
726 if {$build_history_pos < 0} {
727 return
730 if {$busy} return
731 set busy 1
733 set run_nl {}
735 set current_cmd {}
736 set state loading
738 foreach {build_c build_t build_run_s current_cmd} [lindex $build_history $build_history_pos] break
739 $w_entry insert 0 $current_cmd
741 set file_list [list]
742 set current_file_list [list]
743 array unset file_info
744 _reset_diag_list $this
745 set current_path {}
747 foreach i $ui_files_cols {
748 $i conf -state normal
749 $i delete 0.0 end
750 $i conf -state disabled
752 $w_errs conf -width [expr $ui_errs_width + 1]
753 set ui_errs_width_max $ui_errs_width
754 $w_warns conf -width [expr $ui_warns_width + 1]
755 set ui_warns_width_max $ui_warns_width
757 foreach i $ui_hits_cols {
758 $i conf -state normal
759 $i delete 0.0 end
760 $i conf -state disabled
762 $w_lnos conf -width [expr $ui_lnos_width + 1]
763 set ui_lnos_width_max $ui_lnos_width
765 $w_output conf -state normal
766 $w_output delete 0.0 end
767 $w_output conf -state disabled
768 $w_output tag remove found 1.0 end
769 $w_output tag remove currenthit 1.0 end
771 $w_errs conf -width [expr $ui_errs_width + 1]
772 set ui_errs_width_max $ui_warns_width
773 $w_warns conf -width [expr $ui_warns_width + 1]
774 set ui_warns_width_max $ui_warns_width
776 # load infos from build tree
777 set build_load_info [list]
778 set err [catch {
779 set fd [git_read ls-tree $build_t -- cwd exit_status output]
780 while {[gets $fd entry] >= 0} {
781 foreach {infos path} [split $entry "\t"] break
782 foreach {mode type sha1} [split $infos " "] break
783 if {$type ne {blob}} {
784 continue
786 switch -exact $path {
787 cwd {
788 if {$mode eq 120000} {
789 lappend build_load_info [git cat-file blob $sha1]
792 exit_status {
793 if {$mode eq 100644} {
794 lappend build_load_info [git cat-file blob $sha1]
797 output {
798 if {$mode eq 100644} {
799 lappend build_load_info $sha1
804 close $fd
805 } exc]
806 if {$err || [llength $build_load_info] != 3} {
807 return
810 set dir_stack [list [file normalize [file join $::GIT_WORK_TREE [lindex $build_load_info 0]]]]
812 # read output
813 set current_fd [git_read cat-file blob [lindex $build_load_info 2]]
814 fconfigure $current_fd \
815 -blocking 0 \
816 -translation lf
817 fileevent $current_fd readable [cb _read [lindex $build_load_info 1]]
820 method _read {{exit_status {}}} {
821 $w_output conf -state normal
823 while {[gets $current_fd line] >= 0} {
824 set scroll_pos [lindex [$w_output yview] 1]
825 if {$run_nl eq {}} {
826 set mark 1.0
827 } else {
828 set mark [$w_output index end]
831 # pass the original line to git-hash-object
832 if {$exit_status eq {}} {
833 puts $output_hash_out $line
836 # parse color sequences and remove them
837 foreach {line markup} [parse_color_line $line] break
838 set line [string map {\033 ^} $line]
839 regsub {\r$} $line {} line
841 set type out
843 set ipath {}
844 set ipos {}
845 set imsg {}
846 set itype {}
847 if { [regexp -indices {^(.*?): In [^ ]+ .+:} $line imatch ipath]
848 || [regexp -indices {^(?:In file included| ) from (.*?)(?::([0-9]+(?::[0-9]+)?))?[:,]} $line imatch ipath ipos]
849 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: the top level} $line imatch ipath ipos]
850 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: At top level:} $line imatch ipath ipos]
851 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: At global scope:} $line imatch ipath ipos]
852 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: (?:instantiated|required) from .*$} $line imatch ipath ipos]
853 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: ((?:fatal )?(note|warning|WARNING|error): .*)$} $line imatch ipath ipos imsg itype]
854 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: .*? is expanded from...} $line imatch ipath ipos]
855 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: installing [`'].*'} $line imatch ipath ipos]
856 || [regexp -indices {^(.*?):(?:\(.*?\)): ((undefined reference to) [`'].*')} $line imatch ipath imsg itype]
857 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: ((undefined reference to) [`'].*')} $line imatch ipath ipos imsg itype]
858 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: ((required file) [`'].*' not found)} $line imatch ipath ipos imsg itype]
859 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: ([^\s]*? (multiply defined in condition) .* \.\.\.)} $line imatch ipath ipos imsg itype]
860 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: (\.\.\. [`'].*' (previously defined here))} $line imatch ipath ipos imsg itype]
861 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: (.* (does not appear in AM_CONDITIONAL))} $line imatch ipath ipos imsg itype]
862 || [regexp -indices {^(.*?)(?::([0-9]+(?::[0-9]+)?))?: ([`'].*' (included from here))} $line imatch ipath ipos imsg itype]
864 #puts "work dir: $::GIT_WORK_TREE"
865 #puts "current stack: [lindex $dir_stack end]"
866 #puts "output: $line"
868 set path [string range $line [lindex $ipath 0] [lindex $ipath 1]]
869 set orig_path_len [string length $path]
870 #puts "path: $path"
871 set path [file join [lindex $dir_stack end] $path]
872 #puts "full path: $path"
873 set path [file normalize $path]
874 #puts "normalized path: $path"
876 # only remove the gitwork dir prefix, if the normalized path is
877 # actually under the git work tree
878 #puts "work dir prefix: [string range $path 0 [string length $::GIT_WORK_TREE]-1]"
879 if {[string first $::GIT_WORK_TREE $path] == 0} {
880 set path [string range $path [string length $::GIT_WORK_TREE]+1 end]
882 #puts "final path: $path"
883 #puts ""
885 if {$itype eq {}} {
886 set type note
887 } else {
888 set type [string tolower [string range $line [lindex $itype 0] [lindex $itype 1]]]
889 switch -- $type {
890 "undefined reference to" -
891 "required file" -
892 "does not appear in am_conditional" {
893 set type "error"
895 "multiply defined in condition" {
896 set type "warning"
898 "previously defined here" -
899 "included from here" {
900 set type "note"
905 # replace the original path in the output, when the file
906 # exists
907 if {[file exists [file join $::GIT_WORK_TREE $path]]} {
909 if {$type eq "warning" || $type eq "error"} {
911 # the message
912 set msg [string range $line [lindex $imsg 0] [lindex $imsg 1]]
914 # the linepos
915 set pos {}
916 if {$ipos ne {}} {
917 set pos [string range $line [lindex $ipos 0] [lindex $ipos 1]]
920 set cat {}
921 if {[regexp { \[([^[]+)\]$} $msg _ cat]} {
922 _update_diag_list $this $cat
925 # (line in the output, line in the file, type of hit, message)
926 set new_hit [list $mark $pos $type $msg $cat]
928 set exists [array get file_info $path]
929 if {$exists eq {}} {
930 # path unknown, add it
931 array set file_info [list $path [list 0 0 [list]]]
932 # append this file to the list of path
933 lappend file_list $path
935 foreach {p info} [array get file_info $path] break
936 foreach {nwarnings nerrors hits} $info break
938 set hit_exists 0
939 foreach e_hit $hits {
940 foreach {e_mark e_pos e_type e_msg e_cat} $e_hit break
942 if {$pos eq $e_pos
943 && $type eq $e_type
944 && $msg eq $e_msg} {
945 set hit_exists 1
946 break
949 if {!$hit_exists} {
950 if {$type eq "error"} {incr nerrors}
951 if {$type eq "warning"} {incr nwarnings}
953 lappend hits $new_hit
954 set info [list $nwarnings $nerrors $hits]
956 array set file_info [list $path $info]
957 set file_list_needs_update 1
961 set line [string replace $line [lindex $ipath 0] [lindex $ipath 1] $path]
963 # convert ipath and ipos after the original path was replaced
964 set offset [expr [string length $path] - $orig_path_len]
966 set ipath [lreplace $ipath 1 1 [expr [lindex $ipath 1] + $offset]]
968 # shift the ipos only when it is not infront of the path
969 if {$ipos ne {} && [lindex $ipos 0] > [lindex $ipath 0]} {
970 set ipos [list \
971 [expr [lindex $ipos 0] + $offset] \
972 [expr [lindex $ipos 1] + $offset]]
974 } else {
975 # file does not exists, clear ipath and ipos, so that there will
976 # be no tags for them
977 set ipath {}
978 set ipos {}
980 set markup [list]
983 if {[regexp {^.*?: Entering directory [`'](.*)'} $line match path]} {
984 # huh, path maybe empty?
985 if {$path ne {}} {
986 lappend dir_stack [file normalize [file join [lindex $dir_stack end] $path]]
988 set type note
989 set markup [list]
991 if {[regexp {^.*?: Leaving directory [`'](.*)'} $line match path]} {
992 # huh, path maybe empty?
993 if {$path ne {}} {
994 set dir_stack [lrange $dir_stack 0 end-1]
996 set type note
997 set markup [list]
1000 $w_output insert end "$run_nl"
1001 set run_nl "\n"
1003 if {$markup ne {}} {
1004 $w_output insert end "$line"
1005 foreach {posbegin colbegin posend colend} $markup {
1006 set prefix clr
1007 foreach style [lsort -integer [split $colbegin ";"]] {
1008 if {$style eq "7"} {append prefix i; continue}
1009 # ignore bold (1), because it doesn't buy us anything
1010 if {$style != 4
1011 && ($style < 30 || $style > 37)
1012 && ($style < 40 || $style > 47)} {
1013 continue
1015 set a "$mark linestart + $posbegin chars"
1016 set b "$mark linestart + $posend chars"
1017 catch {$w_output tag add $prefix$style $a $b}
1020 } else {
1021 $w_output insert end "$line" $type
1022 if {$ipath ne {}} {
1023 # tag the path
1024 set i1 [$w_output index "$mark + [lindex $ipath 0] chars"]
1025 set i2 [$w_output index "$mark + [lindex $ipath 1] chars + 1 c"]
1026 $w_output tag add path $i1 $i2
1028 # only add the pos tag, when there is a path too
1029 if {$ipos ne {}} {
1030 # tag the pos
1031 set i1 [$w_output index "$mark + [lindex $ipos 0] chars"]
1032 set i2 [$w_output index "$mark + [lindex $ipos 1] chars + 1 c"]
1033 $w_output tag add pos $i1 $i2
1038 if {1.0 == $scroll_pos} {
1039 $w_output yview moveto 1.0
1043 fconfigure $current_fd -blocking 1
1044 if {[eof $current_fd]} {
1045 if {$exit_status eq {}} {
1046 after cancel $build_timer
1047 set build_end [exec date -R]
1048 set build_run_s [expr [exec date +%s -d $build_end] - $build_start_s]
1050 set state committing
1051 set exit_status 0
1052 if {[catch {close $current_fd} err errDict]} {
1053 set exit_status 1
1054 if {[dict get $errDict -code] eq 1} {
1055 set exit_status [lindex [dict get $errDict -errorcode] 2]
1059 # close the fd to git-hash-object, so that we can read the
1060 # result on the other end in
1061 close $output_hash_out
1062 set build_tree(output) [list \
1063 100644 \
1064 blob \
1065 [gets $output_hash_pipe]]
1066 close $output_hash_pipe
1067 set build_tree(exit_status) [list \
1068 100644 \
1069 blob \
1070 [git hash-object -w -t blob --stdin <<$exit_status]]
1072 _commit_build $this
1073 _safe_cmd $this
1075 } else {
1076 catch {close $current_fd}
1079 set file_list_needs_update 1
1081 if {$exit_status} {
1082 set state failed
1083 } else {
1084 set state succeeded
1086 set busy 0
1087 } else {
1088 fconfigure $current_fd -blocking 0
1091 $w_output conf -state disabled
1093 if {$file_list_needs_update} {
1094 _update_file_list $this
1097 } ifdeleted {
1098 catch {close $current_fd}
1099 catch {close $output_hash_out}
1100 catch {gets $output_hash_pipe}
1101 catch {close $output_hash_pipe}
1104 method _update_file_list {} {
1105 # TODO: remeber current position and selection
1106 # we append only, or update the number of errors/warnings
1107 set files_scroll_pos [$w_files yview]
1108 set hits_scroll_pos [$w_hits yview]
1110 if {$file_list_busy} {
1111 return
1113 set file_list_busy 1
1114 set file_list_needs_update 0
1115 set current_file_list [list]
1117 foreach i $ui_files_cols {
1118 $i tag remove in_sel 0.0 end
1119 $i conf -state normal
1120 $i delete 0.0 end
1122 $w_errs conf -width [expr $ui_errs_width + 1]
1123 set ui_errs_width_max $ui_errs_width
1124 $w_warns conf -width [expr $ui_warns_width + 1]
1125 set ui_warns_width_max $ui_warns_width
1127 set fl_nl {}
1129 foreach path $file_list {
1130 foreach {p info} [array get file_info $path] break
1131 foreach {nwarnings nerrors hits} $info break
1133 # filter based on diagnostic type
1134 if {$current_diag == 1} {
1135 if {$nerrors == 0} continue
1136 set nwarnings 0
1137 } elseif {$current_diag == 2} {
1138 if {$nwarnings == 0} continue
1139 set nerrors 0
1140 } elseif {$current_diag > 2} {
1141 set nerrors 0
1142 set nwarnings 0
1143 set diag [lindex $diag_list $current_diag]
1144 foreach hit $hits {
1145 foreach {mark pos type msg cat} $hit break
1146 if {$cat ne $diag} continue
1147 if {$type eq "error"} {incr nerrors}
1148 if {$type eq "warning"} {incr nwarnings}
1150 if {($nerrors + $nwarnings) == 0} continue
1153 foreach i $ui_files_cols {$i insert end "$fl_nl"}
1154 set fl_nl "\n"
1156 lappend current_file_list $path
1158 $w_files insert end "[escape_path $path]"
1160 if {$nerrors == 0} {
1161 set nerrors {}
1163 $w_errs insert end "$nerrors" count
1164 set len [string length $nerrors]
1165 if {$ui_errs_width_max < $len} {
1166 set ui_errs_width_max $len
1169 if {$nwarnings == 0} {
1170 set nwarnings {}
1172 $w_warns insert end "$nwarnings" count
1173 set len [string length $nwarnings]
1174 if {$ui_warns_width_max < $len} {
1175 set ui_warns_width_max $len
1179 if {$ui_errs_width != $ui_errs_width_max} {
1180 $w_errs conf -width [expr $ui_errs_width_max + 1]
1182 if {$ui_warns_width != $ui_warns_width_max} {
1183 $w_warns conf -width [expr $ui_warns_width_max + 1]
1185 foreach i $ui_files_cols {
1186 $i conf -state disabled
1189 # restore position
1190 many2scrollbar $ui_files_cols yview $w_vpane.f.sby \
1191 [lindex $files_scroll_pos 0] \
1192 [lindex $files_scroll_pos 1]
1194 set file_list_busy 0
1196 if {[llength $current_file_list] != 0} {
1197 if {$current_path eq {}} {
1198 # select the first file, when no one is selected
1199 set current_path [lindex $current_file_list 0]
1200 set lno 1
1201 } else {
1202 # restore selection of current path
1203 set lno [lsearch -exact $current_file_list $current_path]
1204 incr lno
1207 foreach i $ui_files_cols {
1208 $i tag add in_sel $lno.0 "$lno.0 + 1 line"
1212 # reaload file and jump to last scroll pos
1213 _show_hits_for_file $this $current_path $hits_scroll_pos
1216 method _select_from_file_list {x y} {
1217 if {$file_list_busy} return
1219 # TODO: remeber scroll pos, when updating the same path?
1221 set lno [lindex [split [$w_files index @0,$y] .] 0]
1222 set path [lindex $current_file_list [expr {$lno - 1}]]
1224 foreach i $ui_files_cols {
1225 $i tag remove in_sel 0.0 end
1228 if {$path eq {}} {
1229 return
1232 foreach i $ui_files_cols {
1233 $i tag add in_sel $lno.0 "$lno.0 + 1 line"
1236 _show_hits_for_file $this $path
1239 method _show_hits_for_file {path {scroll_pos {}}} {
1240 if {$hits_busy} return
1241 set hits_busy 1
1243 set current_path $path
1245 foreach i $ui_hits_cols {
1246 $i conf -state normal
1247 $i delete 0.0 end
1248 $i conf -state disabled
1250 $w_lnos conf -width [expr $ui_lnos_width + 1]
1251 set ui_lnos_width_max $ui_lnos_width
1253 set hits_nl {}
1255 # re-build current_path_lno_hits
1256 array set current_path_lno_hits {}
1258 if {$current_path eq {}} {
1259 set hits_busy 0
1260 return
1263 foreach {p path_info} [array get file_info $current_path] break
1264 foreach {nwarnings nerrors path_hits} $path_info break
1266 set diag [lindex $diag_list $current_diag]
1267 foreach hit $path_hits {
1268 foreach {mark pos type msg cat} $hit break
1270 # filter based on diagnostic type
1271 if { ($current_diag == 1 && $type != "error")
1272 || ($current_diag == 2 && $type != "warning")
1273 || ($current_diag > 2 && $cat ne $diag)} continue
1275 set lno 0
1276 if {$pos ne {}} {
1277 set lno [lindex [split $pos :] 0]
1280 set lno_entry [array get current_path_lno_hits $lno]
1281 if {$lno_entry eq {}} {
1282 # lno unknown, add it
1283 set lno_info [list -1 [list]]
1284 } else {
1285 foreach {_lno lno_info} $lno_entry break
1287 foreach {primary_hit lno_hits} $lno_info break
1288 if {$primary_hit != -1} {
1289 foreach {p_mark p_pos p_type p_msg} [lindex $lno_hits $primary_hit] break
1290 if {$type eq "error" && $p_type eq "warning"} {
1291 set primary_hit [llength $lno_hits]
1293 } else {
1294 set primary_hit 0
1296 lappend lno_hits $hit
1297 array set current_path_lno_hits [list $lno [list $primary_hit $lno_hits]]
1300 set lnos [lsort -integer [array names current_path_lno_hits]]
1302 # insert hits without line information
1303 if {[llength $lnos] > 0 && [lindex $lnos 0] == 0} {
1304 set lnos [lrange $lnos 1 end]
1306 foreach i $ui_hits_cols {$i conf -state normal}
1308 foreach {lno info} [array get current_path_lno_hits 0] break
1309 foreach {primary_hit hits} $info break
1310 foreach hit $hits {
1311 foreach {mark pos type msg cat} $hit break
1313 $w_lnos insert end "$hits_nl"
1314 $w_hits insert end "$hits_nl"
1315 set hits_nl "\n"
1317 $w_hits insert end "$mark" jumpmark
1318 $w_hits insert end "$msg" $type
1321 foreach i $ui_hits_cols {$i conf -state disabled}
1324 if {[llength $lnos] == 0} {
1325 if {$scroll_pos ne {}} {
1326 many2scrollbar $ui_hits_cols yview $w_vpane.o.sby \
1327 [lindex $scroll_pos 0] \
1328 [lindex $scroll_pos 1]
1330 set hits_busy 0
1331 return
1334 set cmd [list | git grep --no-color -h -n -p -3]
1335 set args [list]
1336 foreach lno $lnos {
1337 lappend args -@ $lno
1339 if {[file pathtype $current_path] eq "relative"} {
1340 if {$build_history_pos >= 0} {
1341 foreach {build_c build_t build_run_s _cmd} [lindex $build_history $build_history_pos] break
1342 lappend args "$build_c:worktree"
1344 } else {
1345 set ::GIT_WORK_TREE "/"
1346 lappend cmd --no-index
1348 lappend args -- $current_path
1349 lappend cmd {*}$args
1351 if {[catch {set fd [open $cmd r]} err]} {
1352 git_reset_env
1353 # fallback to GNU nl and GNU grep to get the content
1354 set cmd2 [list | nl -s: -w1 -ba -- $current_path | env -u GREP_OPTIONS grep --color=never -C 3]
1355 foreach lno $lnos {
1356 lappend cmd2 -e "^$lno:"
1358 if {[catch {set fd [open $cmd2 r]} err2]} {
1359 tk_messageBox \
1360 -icon error \
1361 -type ok \
1362 -title {gui-grep: fatal error} \
1363 -message "failed: $cmd\n$err\n\nfallback failed: $cmd2\n$err2"
1364 set hits_busy 0
1365 return
1367 set cmd $cmd2
1369 git_reset_env
1371 fconfigure $fd -eofchar {}
1372 fconfigure $fd \
1373 -blocking 0 \
1374 -buffering full \
1375 -buffersize 512 \
1376 -translation lf
1377 fileevent $fd readable [cb _read_file $fd $scroll_pos]
1380 method _read_file {fd scroll_pos} {
1381 foreach i $ui_hits_cols {$i conf -state normal}
1383 while {[gets $fd line] >= 0} {
1385 set mark {}
1386 set auxmsg {}
1388 # catch hunk sep --
1389 if {[regexp {^--} $line]} {
1390 set lno "--"
1391 set line {}
1392 } else {
1393 # remove any color from lno and sep
1394 regexp {^(\d+)([-:=])(.*)$} $line match lno line_type line
1395 regsub {\r$} $line {} line
1396 if { $line_type eq {:}
1397 && [array get current_path_lno_hits $lno] ne {}} {
1398 foreach {lno info} [array get current_path_lno_hits $lno] break
1399 foreach {primary_hit hits} $info break
1401 # the actual line has the mark and the aux message of the primamry hit
1402 foreach {mark h_lno h_type auxmsg} [lindex $hits $primary_hit] break
1404 foreach hit $hits {
1405 foreach {h_mark h_pos h_type h_msg h_cat} $hit break
1407 $w_lnos insert end "$hits_nl"
1408 $w_hits insert end "$hits_nl"
1409 set hits_nl "\n"
1411 $w_lnos insert end "$h_pos" $h_type
1412 set len [string length $h_pos]
1413 if {$ui_lnos_width_max < $len} {
1414 set ui_lnos_width_max $len
1417 $w_hits insert end "$h_mark" jumpmark
1418 $w_hits insert end "$h_msg" $h_type
1423 $w_lnos insert end "$hits_nl"
1424 $w_hits insert end "$hits_nl"
1425 set hits_nl "\n"
1427 $w_lnos insert end "$lno" normal
1428 set len [string length $lno]
1429 if {$ui_lnos_width_max < $len} {
1430 set ui_lnos_width_max $len
1433 if {$mark ne {}} {
1434 $w_hits insert end "$mark" jumpmark
1436 if {$auxmsg ne {}} {
1437 $w_hits insert end "$auxmsg" auxmark
1439 $w_hits insert end "$line" normal
1442 if {[eof $fd]} {
1443 close $fd
1445 #update line number column width
1446 if {$ui_lnos_width != $ui_lnos_width_max} {
1447 $w_lnos conf -width [expr $ui_lnos_width_max + 1]
1450 if {$scroll_pos ne {}} {
1451 many2scrollbar $ui_hits_cols yview $w_vpane.o.sby \
1452 [lindex $scroll_pos 0] \
1453 [lindex $scroll_pos 1]
1456 set hits_busy 0
1459 foreach i $ui_hits_cols {$i conf -state disabled}
1461 } ifdeleted {
1462 catch {close $fd}
1465 method _jump_to_hit_in_output {x y} {
1466 if {$hits_busy} {
1467 return
1470 if {$current_path eq {}} {
1471 return
1474 set imark [$w_hits tag nextrange jumpmark "@$x,$y linestart" "@$x,$y lineend"]
1475 if {$imark eq {}} {
1476 return
1479 set mark [$w_hits get [lindex $imark 0] [lindex $imark 1]]
1480 $w_output see $mark
1481 $w_output tag remove currenthit 0.0 end
1482 $w_output tag add currenthit "$mark linestart" "$mark lineend"
1485 method _open_from_hits {x y} {
1486 if {$hits_busy} {
1487 return
1490 if {$current_path eq {}} {
1491 return
1494 set lno 0
1495 set aux {}
1496 set wlno [$w_lnos search -regexp {^[[:digit:]]+(?::[[:digit:]]+)?$} "@0,$y linestart" end]
1497 if {$wlno eq {}} {
1498 set wlno [$w_lnos search -backwards -regexp {^[[:digit:]]+(?::[[:digit:]]+)?$} "@0,$y linestart" 1.0]
1500 if {$wlno ne {}} {
1501 set lno [lindex [split [$w_lnos get "$wlno" "$wlno lineend"] :] 0]
1502 set iaux [$w_hits tag nextrange auxmark "@$x,$y linestart" "@$x,$y lineend"]
1503 if {$iaux ne {}} {
1504 set aux [$w_hits get [lindex $iaux 0] [lindex $iaux 1]]
1505 } else {
1506 set aux [$w_hits get -displaychars "@0,$y linestart" "@0,$y lineend"]
1510 open_in_git_editor $current_path $lno 0 $aux
1513 method _safe_cmd {} {
1514 set cmd [$w_entry get]
1515 $w_entry delete 0 end
1516 if {[lindex $cmd_history 1] ne $cmd} {
1517 set cmd_history [linsert $cmd_history 1 $cmd]
1518 set cmd_history [lreplace $cmd_history 0 0 ""]
1520 set cmd_history_pos 0
1523 method _prev_cmd {} {
1524 if {[expr [llength $cmd_history] - 1] > $cmd_history_pos} {
1525 if {$cmd_history_pos == 0} {
1526 set cmd_history [lreplace $cmd_history 0 0 [$w_entry get]]
1528 incr cmd_history_pos
1529 $w_entry delete 0 end
1530 $w_entry insert 0 [lindex $cmd_history $cmd_history_pos]
1534 method _next_cmd {} {
1535 if {0 < $cmd_history_pos} {
1536 incr cmd_history_pos -1
1537 $w_entry delete 0 end
1538 $w_entry insert 0 [lindex $cmd_history $cmd_history_pos]
1542 method _search_prev_cmd {} {
1543 set cursor [$w_entry index insert]
1544 if {$cursor == 0} {
1545 _prev_cmd $this
1546 $w_entry icursor $cursor
1547 return
1549 incr cursor -1
1550 set prefix [string range [$w_entry get] 0 $cursor]
1551 for {set i [expr $cmd_history_pos + 1]} {$i < [llength $cmd_history]} {incr i} {
1552 set cmd [lindex $cmd_history $i]
1553 if {[string range $cmd 0 $cursor] eq $prefix} {
1554 set cmd_history_pos $i
1555 $w_entry delete 0 end
1556 $w_entry insert 0 $cmd
1557 $w_entry icursor [expr $cursor + 1]
1558 break
1563 method _search_next_cmd {} {
1564 set cursor [$w_entry index insert]
1565 if {$cursor == 0} {
1566 _next_cmd $this
1567 $w_entry icursor $cursor
1568 return
1570 incr cursor -1
1571 set prefix [string range [$w_entry get] 0 $cursor]
1572 for {set i [expr $cmd_history_pos - 1]} {$i > 0} {incr i -1} {
1573 set cmd [lindex $cmd_history $i]
1574 if {[string range $cmd 0 $cursor] eq $prefix} {
1575 set cmd_history_pos $i
1576 $w_entry delete 0 end
1577 $w_entry insert 0 $cmd
1578 $w_entry icursor [expr $cursor + 1]
1579 break
1584 method _prev_build {} {
1585 if {[llength $build_history] > ($build_history_pos + 1)} {
1586 incr build_history_pos
1587 _load $this
1591 method _next_build {} {
1592 if {0 < $build_history_pos} {
1593 incr build_history_pos -1
1594 _load $this
1598 method _cancel {} {
1599 if {$build_history_pos >= 0} return
1600 if {$busy} {
1601 after cancel $build_timer
1602 set build_end [exec date -R]
1603 set build_run_s [expr [exec date +%s -d $build_end] - $build_start_s]
1605 set state canceling
1606 fconfigure $current_fd -blocking 1
1607 foreach p [pid $current_fd] {
1608 catch {exec kill $p}
1610 catch {close $output_hash_out}
1611 catch {gets $output_hash_pipe}
1612 catch {close $output_hash_pipe}
1613 set is_err 0
1614 if {[catch {close $current_fd} err opts]} {
1615 set details [dict get $opts -errorcode]
1616 # we killed the child ourself, don't handle this as an error
1617 if { [lindex $details 0] ne {CHILDKILLED}
1618 && [lindex $details 2] ne {SIGKILL}} {
1619 set is_err 1
1623 if {$is_err} {
1624 set state failed
1625 } else {
1626 set state succeeded
1628 set busy 0
1632 method _select_diagnostics {} {
1633 $w_diags selection clear
1634 set current_diag [$w_diags current]
1635 set file_list_needs_update 1
1636 _update_file_list $this
1639 method _reset_diag_list {} {
1640 set diag_list [list "All" "Errors" "Warnings"]
1641 set current_diag 0
1642 set width 0
1643 foreach diag $diag_list {
1644 set cx [string length $diag]
1645 if {$cx > $width} {set width $cx}
1647 $w_diags configure -values $diag_list
1648 $w_diags configure -width $width
1649 $w_diags current $current_diag
1652 method _update_diag_list {diag} {
1653 set exists [lsearch -exact $diag_list $diag]
1654 if {$exists != -1} return
1656 lappend diag_list $diag
1657 $w_diags configure -values $diag_list
1659 set cx [string length $diag]
1660 if {$cx > [$w_diags cget -width]} {
1661 $w_diags configure -width $cx
1663 $w_diags current $current_diag
1666 method _always_takefocus {w} {
1667 return 1
1670 method reorder_bindtags {} {
1671 foreach i [list $w_entry] {
1672 bindtags $i [list all $i [winfo class $i] .]
1676 method link_vpane {vpane} {
1677 bind $w_vpane <Map> [cb _on_pane_mapped $vpane]
1680 method _on_pane_mapped {master_vpane} {
1681 if {$::use_ttk} {
1682 after idle [list after idle [list $w_vpane sashpos 0 [$master_vpane sashpos 0]]]
1683 } else {
1684 after idle [list after idle \
1685 [list $w_vpane sash place 0 \
1686 [lindex [$master_vpane sash coord 0] 0] \
1687 [lindex [$w_vpane sash coord 0] 1]]]
1691 method _open_from_output {x y} {
1692 set ipath [$w_output tag nextrange path "@$x,$y linestart" "@$x,$y lineend"]
1693 if {$ipath ne {}} {
1694 set path [$w_output get [lindex $ipath 0] [lindex $ipath 1]]
1696 set lno 0
1697 set ipos [$w_output tag nextrange pos "@$x,$y linestart" "@$x,$y lineend"]
1698 if {$ipos ne {}} {
1699 set pos [$w_output get [lindex $ipos 0] [lindex $ipos 1]]
1700 set lno [lindex [split $pos :] 0]
1703 open_in_git_editor $path $lno
1707 method _update_path_label {args} {
1708 # any change to current_path makes the current_path_lno_hits array invalid
1709 array unset current_path_lno_hits
1711 if {$current_path eq {}} {
1712 set current_path_label ""
1713 } else {
1714 set current_path_label "File: [escape_path $current_path]"
1718 method _copy_path {} {
1719 clipboard clear
1720 clipboard append \
1721 -format STRING \
1722 -type STRING \
1723 -- $current_path
1726 method _update_cmd_label {args} {
1727 $w_entry conf -state normal
1728 if {$current_cmd eq {}} {
1729 set current_cmd_label ""
1730 } else {
1731 switch $state {
1732 running {
1733 set current_cmd_label "[_get_runtime $this] Running command: $current_cmd"
1734 $w_entry conf -state disabled
1735 $w_indicator conf -foreground white -background blue
1737 succeeded {
1738 set current_cmd_label "[_get_runtime $this] Successful command: $current_cmd"
1739 $w_indicator conf -foreground black -background green
1741 failed {
1742 set current_cmd_label "[_get_runtime $this] Failed command: $current_cmd"
1743 $w_indicator conf -foreground black -background red
1745 canceling {
1746 set current_cmd_label "[_get_runtime $this] Canceling command: $current_cmd"
1747 $w_indicator conf -foreground black -background cyan
1749 committing {
1750 set current_cmd_label "[_get_runtime $this] Committing command: $current_cmd"
1751 $w_indicator conf -foreground black -background orange
1753 loading {
1754 set current_cmd_label "[_get_runtime $this] Loading command: $current_cmd"
1755 $w_entry conf -state disabled
1756 $w_indicator conf -foreground white -background blue
1760 if {$state_change_cb ne {}} {
1761 eval $state_change_cb $state
1766 method _get_runtime {} {
1767 set m [expr $build_run_s / 60]
1768 set s [expr $build_run_s % 60]
1769 if {$s < 10} {
1770 set s "0$s"
1772 if {$build_history_pos >= 0
1773 && ![catch {set relative_start "[exec git log -1 -g --pretty=%ar $build_ref@{$build_history_pos}]"} err]} {
1774 return "@{$relative_start} \[$m:$s\]"
1776 return "\[$m:$s\]"
1779 method _update_runtime {} {
1780 set new_build_run_s [expr [exec date +%s] - $build_start_s]
1781 if {$new_build_run_s > $build_run_s} {
1782 set build_run_s $new_build_run_s
1783 set state $state
1785 set build_timer [after 250 [cb _update_runtime]]
1788 method _commit_build {} {
1789 set ls_tree {}
1790 foreach name [array names static_build_tree] {
1791 foreach {mode type hash} $static_build_tree($name) break
1792 append ls_tree "$mode $type $hash\t$name\n"
1794 foreach name [array names build_tree] {
1795 foreach {mode type hash} $build_tree($name) break
1796 append ls_tree "$mode $type $hash\t$name\n"
1798 set tree [git mktree <<$ls_tree]
1800 set cmd [$w_entry get]
1801 set cmdn "$cmd\n"
1803 set restore_envs [modify_env [list \
1804 GIT_AUTHOR_DATE 1 $build_start \
1805 GIT_COMMITTER_DATE 1 $build_end \
1808 set commit [git commit-tree $tree <<$cmdn]
1810 restore_env $restore_envs
1812 set reflog [gitdir logs $build_ref]
1813 if {[file exists $reflog] || ![catch {
1814 file mkdir [file dirname $reflog]
1815 set fd [open $reflog a]
1816 close $fd
1817 }]} {
1818 exec git update-ref -m $cmd $build_ref $commit
1821 set build_history [linsert $build_history 0 [list \
1822 $commit \
1823 $tree \
1824 $build_run_s \
1825 $cmd]]
1826 set build_history_pos 0
1829 method _files_scroll_line {dir} {
1830 if {$file_list_busy} return
1832 if {[catch {$w_files index in_sel.first}]} {
1833 return
1836 set lno [lindex [split [$w_files index in_sel.first] .] 0]
1837 incr lno $dir
1839 set path [lindex $current_file_list [expr {$lno - 1}]]
1840 if {$path eq {}} {
1841 return
1844 foreach i $ui_files_cols {
1845 $i tag remove in_sel 0.0 end
1846 $i tag add in_sel $lno.0 "$lno.0 + 1 line"
1848 $w_files see $lno.0
1850 _show_hits_for_file $this $path
1853 method _files_scroll_page {dir} {
1854 if {$file_list_busy} return
1856 $w_files yview scroll $dir pages
1857 set lno [expr {int(
1858 [lindex [$w_files yview] 0]
1859 * [llength $current_file_list]
1860 + 1)}]
1862 set path [lindex $current_file_list [expr {$lno - 1}]]
1863 if {$path eq {}} {
1864 return
1867 foreach i $ui_files_cols {
1868 $i tag remove in_sel 0.0 end
1869 $i tag add in_sel $lno.0 "$lno.0 + 1 line"
1871 $w_files see $lno.0
1873 _show_hits_for_file $this $path
1876 method _visible {} {
1877 if {[$ui_finder visible]} {
1878 focus [$ui_finder editor]
1879 } else {
1880 focus $w_entry
1884 method _get_envmods {} {
1885 set env_mods $envmods
1886 _load_config $this
1887 foreach config $configmods {
1888 foreach mod [_get_buildconfig $this $config] {
1889 if {[regexp {^([A-Za-z0-9_]+)\s*((?:[-+%].?)?=)\s*(.*)$} $mod match name op value]} {
1890 lappend env_mods $name $op $value
1891 } elseif {[regexp {^!([A-Za-z0-9_]+)$} $mod match name]} {
1892 lappend env_mods $name ! {}
1896 return $env_mods
1899 method _popup_configs {X Y} {
1900 if {$busy} return
1902 _load_config $this
1904 $m_configs delete 0 end
1906 array unset selected_configs
1908 array unset menu_hierarchy
1909 # build the menu hierarchy first without actually entries
1910 foreach full_config [lsort [array names buildconfig_config gui.buildconfig.*.env]] {
1911 set config [string range $full_config [string length {gui.buildconfig.}] end-[string length {.env}]]
1913 set names [split $config "/"]
1914 set parent $m_configs
1915 for {set i 0} {$i < [llength $names]-1} {incr i} {
1916 set subname [join [lrange $names 0 $i] "/"]
1918 if {![info exists menu_hierarchy($subname)]} {
1919 set subid $parent.t[incr buildconfig_menu_id]
1920 $parent add cascade \
1921 -label [lindex $names $i] \
1922 -menu $subid
1923 menu $subid -tearoff 0
1924 $parent index end
1925 set menu_hierarchy($subname) $subid
1927 set parent $menu_hierarchy($subname)
1931 foreach full_config [lsort [array names buildconfig_config gui.buildconfig.*.env]] {
1932 set config [string range $full_config [string length {gui.buildconfig.}] end-[string length {.env}]]
1933 if {[lsearch -exact $configmods $config] >= 0} {
1934 set selected_configs($config) 1
1935 } else {
1936 set selected_configs($config) 0
1939 set names [split $config "/"]
1940 set parent $m_configs
1941 for {set i 0} {$i < [llength $names]-1} {incr i} {
1942 set subname [join [lrange $names 0 $i] "/"]
1943 set parent $menu_hierarchy($subname)
1946 if {[info exists menu_hierarchy($config)]} {
1947 $menu_hierarchy($config) insert 0 separator
1948 $menu_hierarchy($config) insert 0 checkbutton \
1949 -label [_get_buildconfig $this $config title "This"] \
1950 -command [cb _update_configs_from_menu] \
1951 -variable ${__this}::selected_configs($config) \
1952 -onvalue 1 \
1953 -offvalue 0
1954 } else {
1955 $parent add checkbutton \
1956 -label [_get_buildconfig $this $config title [lindex $names end]] \
1957 -command [cb _update_configs_from_menu] \
1958 -variable ${__this}::selected_configs($config) \
1959 -onvalue 1 \
1960 -offvalue 0
1963 if {[array size selected_configs] > 0} {
1964 tk_popup $m_configs $X $Y
1968 method _load_config {} {
1969 global repo_config
1970 load_config 0
1971 array unset buildconfig_config
1972 # make this a list and iterate over all commands
1973 set cmd [get_config gui.build.configcommand {}]
1974 if {$cmd ne {}} {
1975 _parse_config buildconfig_config [list open_read $cmd]
1977 foreach name [array names repo_config] {
1978 if {[catch {set v $buildconfig_config($name)}]} {
1979 set buildconfig_config($name) $repo_config($name)
1984 method _get_buildconfig {config {var {env}} {default {}}} {
1985 if {[catch {set v $buildconfig_config(gui.buildconfig.$config.$var)}]} {
1986 return $default
1987 } else {
1988 return $v
1992 method _update_configs_from_menu {} {
1993 set configmods [list]
1994 foreach config [array names selected_configs] {
1995 if {$selected_configs($config)} {
1996 lappend configmods $config
2003 proc envargs {envmods} {
2004 global env
2005 array set newenv [array get env]
2007 foreach {name op value} $envmods {
2008 set old {}
2009 if {[info exists newenv($name)]} {
2010 # remember, that the variable was set
2011 set old $newenv($name)
2013 set sep ""
2014 if {![regexp {([-+%])(.)=} $op match op sep]} {
2015 regexp {([-+%])=} $op match op
2017 if {[string first $op "-+%"] >= 0 && $old ne {}} {
2018 if {$sep ne ""} {
2019 set l [split $old $sep]
2020 set e [lsearch -exact $l $value]
2021 while {$e >= 0} {
2022 set l [concat [lrange $l 0 $e-1] [lrange $l $e+1 end]]
2023 set e [lsearch -exact $l $value]
2025 set old [join $l $sep]
2026 } else {
2027 set f [string first $value $old]
2028 while {$f >= 0} {
2029 set old [string replace $old $f $f+[string length $value]]
2030 set f [string first $value $old]
2033 if {$op eq "+"} {
2034 set newenv($name) ${old}${sep}${value}
2035 } elseif {$op eq "%"} {
2036 set newenv($name) ${value}${sep}${old}
2037 } else {
2038 # {$op eq "-"}
2039 set newenv($name) ${old}
2041 } elseif {$op eq "!"} {
2042 catch {unset newenv($name)}
2043 } else {
2044 set newenv($name) $value
2048 set env_args [list]
2049 foreach name [array names env] {
2050 if {![info exists newenv($name)]} {
2051 lappend env_args -u $name
2054 foreach name [array names newenv] {
2055 if {![info exists env($name)]} {
2056 lappend env_args $name=$newenv($name)
2057 } elseif {$env($name) ne $newenv($name)} {
2058 lappend env_args $name=$newenv($name)
2061 unset newenv
2063 return $env_args
2066 proc modify_env {envmods} {
2067 global env
2069 set restore_envs [list]
2070 foreach {name set value} $envmods {
2071 if {[info exists env($name)]} {
2072 # remember, that the variable was set
2073 lappend restore_envs $name 1 $env($name)
2074 } else {
2075 lappend restore_envs $name 0 {}
2077 if {$set} {
2078 set env($name) $value
2079 } else {
2080 catch {unset env($name)}
2083 return $restore_envs
2086 proc restore_env {envrestores} {
2087 global env
2088 foreach {name set value} $envrestores {
2089 if {$set} {
2090 set env($name) $value
2091 } else {
2092 unset env($name)