* [PATCH] gitk: add user-defined custom commands
@ 2026-08-04 21:43 Tim Wiederhake via GitGitGadget
2026-08-05 6:59 ` Johannes Sixt
2026-08-07 22:16 ` Junio C Hamano
0 siblings, 2 replies; 4+ messages in thread
From: Tim Wiederhake via GitGitGadget @ 2026-08-04 21:43 UTC (permalink / raw)
To: git; +Cc: Tim Wiederhake, Tim Wiederhake
From: Tim Wiederhake <twied@gmx.net>
Allow users to define up to three custom commands each for the commit
list and the diff display area. Commands are configured in a new
"Commands" tab in the preferences dialog, with a name and a command
template per slot. Non-empty slots appear in the right-click context
menu of the respective area.
Command templates support placeholder substitution (commit id, commit
title, author name, author date, etc.) and are executed via "sh -c"
to allow for background execution by appending "&", and pipeing. If
a command terminates with exit code 42, its output is displayed;
otherwise only non-zero exit codes are reported.
Signed-off-by: Tim Wiederhake <twied@gmx.net>
Assisted-by: Claude Opus 4.6
---
gitk: add user-defined custom commands
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2371%2Ftwied%2Fcustom_commands-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2371/twied/custom_commands-v1
Pull-Request: https://github.com/git/git/pull/2371
gitk-git/gitk | 220 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 217 insertions(+), 3 deletions(-)
diff --git a/gitk-git/gitk b/gitk-git/gitk
index 0f3571050b..af9cfd9eec 100755
--- a/gitk-git/gitk
+++ b/gitk-git/gitk
@@ -3696,9 +3696,11 @@ proc find_ctext_fileinfo {line} {
}
proc pop_diff_menu {w X Y x y} {
- global ctext diff_menu flist_menu_file
- global diff_menu_txtpos diff_menu_line
- global diff_menu_filebase
+ global ctext diff_menu flist_menu_file currentid
+ global diff_menu_txtpos diff_menu_line diff_menu_filebase
+ global usercmd_dd_name1 usercmd_dd_body1
+ global usercmd_dd_name2 usercmd_dd_body2
+ global usercmd_dd_name3 usercmd_dd_body3
set diff_menu_txtpos [split [$w index "@$x,$y"] "."]
set diff_menu_line [lindex $diff_menu_txtpos 0]
@@ -3711,6 +3713,10 @@ proc pop_diff_menu {w X Y x y} {
if {$f eq {}} return
set flist_menu_file [lindex $f 0]
set diff_menu_filebase [lindex $f 1]
+ update_menu_custom_cmds $diff_menu $currentid \
+ [list $usercmd_dd_name1 $usercmd_dd_body1 \
+ $usercmd_dd_name2 $usercmd_dd_body2 \
+ $usercmd_dd_name3 $usercmd_dd_body3]
tk_popup $diff_menu $X $Y
}
@@ -9148,9 +9154,134 @@ proc mstime {} {
return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]]
}
+proc update_menu_custom_cmds {menu id cmds} {
+ if {![info exists ::usercmd_menu_count($menu)]} {
+ set ::usercmd_menu_count($menu) 0
+ }
+
+ for {set j 0} {$j < $::usercmd_menu_count($menu)} {incr j} {
+ $menu delete end
+ }
+
+ set ::usercmd_menu_count($menu) 0
+ foreach {name cmd_template} $cmds {
+ if {$name ne "" && $cmd_template ne ""} {
+ if {$::usercmd_menu_count($menu) == 0} {
+ $menu add separator
+ incr ::usercmd_menu_count($menu)
+ }
+ $menu add command -label $name \
+ -command [list exec_custom_cmd $cmd_template $id]
+ incr ::usercmd_menu_count($menu)
+ }
+ }
+}
+
+proc get_blame_origin {} {
+ global diff_menu_filebase diff_menu_line flist_menu_file
+ global currentid cmitmode parents curview cdup
+
+ set blame_id ""
+ set blame_line ""
+ catch {
+ if {$cmitmode eq "tree"} {
+ set line [expr {$diff_menu_line - $diff_menu_filebase}]
+ set blamefile [file join $cdup $flist_menu_file]
+ set blame_out [exec git blame -p -L$line,+1 $currentid -- $blamefile]
+ set first [lindex [split $blame_out "\n"] 0]
+ set blame_id [lindex $first 0]
+ set blame_line [lindex $first 1]
+ } else {
+ set h [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
+ if {$h ne {}} {
+ set pi [lindex $h 0]
+ if {$pi > 0} {
+ incr pi -1
+ set blame_parent [lindex $parents($curview,$currentid) $pi]
+ set line [lindex $h 1]
+ set blamefile [file join $cdup $flist_menu_file]
+ set blame_out [exec git blame -p -L$line,+1 $blame_parent -- $blamefile]
+ set first [lindex [split $blame_out "\n"] 0]
+ set blame_id [lindex $first 0]
+ set blame_line [lindex $first 1]
+ }
+ }
+ }
+ }
+ return [list $blame_id $blame_line]
+}
+
+proc get_diff_file {} {
+ global flist_menu_file
+ if {[info exists flist_menu_file]} {
+ return $flist_menu_file
+ }
+ return ""
+}
+
+proc exec_custom_cmd {cmd_template id} {
+ global commitinfo markedid
+
+ getcommit $id
+
+ set blame_computed 0
+ set blame_id ""
+ set blame_line ""
+
+ set cmd ""
+ set len [string length $cmd_template]
+ for {set i 0} {$i < $len} {incr i} {
+ if {[string index $cmd_template $i] eq "%" && $i + 1 < $len} {
+ set next [string index $cmd_template [expr {$i + 1}]]
+ if {!$blame_computed && ($next eq "b" || $next eq "l")} {
+ set blame [get_blame_origin]
+ set blame_id [lindex $blame 0]
+ set blame_line [lindex $blame 1]
+ set blame_computed 1
+ }
+ switch -- $next {
+ "%" { append cmd "%" }
+ "i" { append cmd $id }
+ "t" { append cmd [lindex $commitinfo($id) 0] }
+ "a" { append cmd [lindex $commitinfo($id) 1] }
+ "d" { append cmd [lindex $commitinfo($id) 2] }
+ "c" { append cmd [lindex $commitinfo($id) 3] }
+ "D" { append cmd [lindex $commitinfo($id) 4] }
+ "m" { append cmd [lindex $commitinfo($id) 5] }
+ "M" { if {[info exists markedid]} { append cmd $markedid } }
+ "b" { append cmd $blame_id }
+ "f" { append cmd [get_diff_file] }
+ "l" { append cmd $blame_line }
+ default { append cmd "%" $next }
+ }
+ incr i
+ } else {
+ append cmd [string index $cmd_template $i]
+ }
+ }
+
+ if {[catch {exec sh -c $cmd 2>@1} output]} {
+ set exitcode 1
+ if {[lindex $::errorCode 0] eq "CHILDSTATUS"} {
+ set exitcode [lindex $::errorCode 2]
+ }
+ if {$exitcode == 42} {
+ tk_messageBox -type ok -icon info \
+ -title [mc "Command output"] -message $output
+ } else {
+ tk_messageBox -type ok -icon error \
+ -title [mc "Command failed"] \
+ -message [mc "Exit code %d:\n%s" $exitcode $output]
+ }
+ }
+}
+
proc rowmenu {x y id} {
global rowctxmenu selectedline rowmenuid curview
global nullid nullid2 fakerowmenu mainhead markedid
+ global usercmd_cl_name1 usercmd_cl_body1
+ global usercmd_cl_name2 usercmd_cl_body2
+ global usercmd_cl_name3 usercmd_cl_body3
stopfinding
set rowmenuid $id
@@ -9182,6 +9313,10 @@ proc rowmenu {x y id} {
$menu entryconfigure [mca "Make patch"] -state $state
$menu entryconfigure [mca "Diff this -> marked commit"] -state $mstate
$menu entryconfigure [mca "Diff marked commit -> this"] -state $mstate
+ update_menu_custom_cmds $menu $id \
+ [list $usercmd_cl_name1 $usercmd_cl_body1 \
+ $usercmd_cl_name2 $usercmd_cl_body2 \
+ $usercmd_cl_name3 $usercmd_cl_body3]
tk_popup $menu $x $y
}
@@ -11916,6 +12051,60 @@ proc prefspage_fonts {notebook} {
return $page
}
+proc prefspage_commands {notebook} {
+ global {*}$::config_variables
+
+ set page [create_prefs_page $notebook.commands]
+
+ ttk::label $page.cl_header -text [mc "Commit list"] -font mainfontbold
+ grid $page.cl_header - - -sticky w -pady 10
+
+ ttk::label $page.cl_namelbl -text [mc "Name"]
+ ttk::label $page.cl_cmdlbl -text [mc "Command"]
+ grid x $page.cl_namelbl $page.cl_cmdlbl -sticky w
+
+ foreach i {1 2 3} {
+ ttk::label $page.cl_row${i}lbl -text "${i}."
+ ttk::entry $page.cl_name${i} -textvariable usercmd_cl_name${i} -width 20
+ ttk::entry $page.cl_body${i} -textvariable usercmd_cl_body${i} -width 40
+ grid $page.cl_row${i}lbl $page.cl_name${i} $page.cl_body${i} -sticky ew -padx 2
+ }
+
+ ttk::label $page.dd_header -text [mc "Diff display"] -font mainfontbold
+ grid $page.dd_header - - -sticky w -pady 10
+
+ ttk::label $page.dd_namelbl -text [mc "Name"]
+ ttk::label $page.dd_cmdlbl -text [mc "Command"]
+ grid x $page.dd_namelbl $page.dd_cmdlbl -sticky w
+
+ foreach i {1 2 3} {
+ ttk::label $page.dd_row${i}lbl -text "${i}."
+ ttk::entry $page.dd_name${i} -textvariable usercmd_dd_name${i} -width 20
+ ttk::entry $page.dd_body${i} -textvariable usercmd_dd_body${i} -width 40
+ grid $page.dd_row${i}lbl $page.dd_name${i} $page.dd_body${i} -sticky ew -padx 2
+ }
+
+ set explain "Commands with both name and command filled in will "
+ append explain "appear in the context menu (right-click) of the "
+ append explain "respective area. "
+ append explain "Substitution: %% = literal %, %i = commit id, "
+ append explain "%t = title, %m = message, %a = author, "
+ append explain "%d = author date, %c = committer, "
+ append explain "%D = committer date, %M = marked commit id, "
+ append explain "%f = file path (diff only), "
+ append explain "%b = blame origin id (diff only), "
+ append explain "%l = blame origin line number (diff only). "
+ append explain "Exit code 0 = silent; 42 = show output; "
+ append explain "other = show error. "
+ append explain "Append \"&\" to run asynchronously."
+ ttk::label $page.explain -text $explain -wraplength 500 -justify left
+ grid $page.explain - - -sticky w -pady 10 -padx 5
+
+ grid columnconfigure $page 2 -weight 1
+
+ return $page
+}
+
proc doprefs {} {
global oldprefs prefstop
global {*}$::config_variables
@@ -11938,6 +12127,7 @@ proc doprefs {} {
lappend pages [prefspage_general $notebook] [mc "General"]
lappend pages [prefspage_colors $notebook] [mc "Colors"]
lappend pages [prefspage_fonts $notebook] [mc "Fonts"]
+ lappend pages [prefspage_commands $notebook] [mc "Commands"]
set col 0
foreach {page title} $pages {
$notebook add $page -text $title
@@ -12659,6 +12849,18 @@ set autocopy 0
set autoselect 1
set autosellen $hashlength
set perfile_attrs 0
+set usercmd_cl_name1 ""
+set usercmd_cl_body1 ""
+set usercmd_cl_name2 ""
+set usercmd_cl_body2 ""
+set usercmd_cl_name3 ""
+set usercmd_cl_body3 ""
+set usercmd_dd_name1 ""
+set usercmd_dd_body1 ""
+set usercmd_dd_name2 ""
+set usercmd_dd_body2 ""
+set usercmd_dd_name3 ""
+set usercmd_dd_body3 ""
if {[tk windowingsystem] eq "aqua"} {
set extdifftool "opendiff"
@@ -12807,6 +13009,18 @@ set config_variables {
uifgcolor
uifgdisabledcolor
uifont
+ usercmd_cl_body1
+ usercmd_cl_body2
+ usercmd_cl_body3
+ usercmd_cl_name1
+ usercmd_cl_name2
+ usercmd_cl_name3
+ usercmd_dd_body1
+ usercmd_dd_body2
+ usercmd_dd_body3
+ usercmd_dd_name1
+ usercmd_dd_name2
+ usercmd_dd_name3
visiblerefs
web_browser
workingfilescirclecolor
base-commit: 5b2471720c93ee30e5764a19f3d3b3ae9ec9712a
--
gitgitgadget
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] gitk: add user-defined custom commands
2026-08-04 21:43 [PATCH] gitk: add user-defined custom commands Tim Wiederhake via GitGitGadget
@ 2026-08-05 6:59 ` Johannes Sixt
2026-08-07 21:39 ` Tim Wiederhake
2026-08-07 22:16 ` Junio C Hamano
1 sibling, 1 reply; 4+ messages in thread
From: Johannes Sixt @ 2026-08-05 6:59 UTC (permalink / raw)
To: Tim Wiederhake; +Cc: git, Tim Wiederhake via GitGitGadget
Am 04.08.26 um 23:43 schrieb Tim Wiederhake via GitGitGadget:
> Allow users to define up to three custom commands each for the commit
> list and the diff display area. Commands are configured in a new
> "Commands" tab in the preferences dialog, with a name and a command
> template per slot. Non-empty slots appear in the right-click context
> menu of the respective area.
>
> Command templates support placeholder substitution (commit id, commit
> title, author name, author date, etc.) and are executed via "sh -c"
> to allow for background execution by appending "&", and pipeing. If
> a command terminates with exit code 42, its output is displayed;
> otherwise only non-zero exit codes are reported.
Thanks, but this commit message is a bit lacking: it does not explain
why it is a good idea to have this feature, what purpose it servers. For
this reason, it is not possible to tell whether the design is sound and
whether the implementation follows the design.
> Signed-off-by: Tim Wiederhake <twied@gmx.net>
> Assisted-by: Claude Opus 4.6
Thank you for being explicit about AI assistance. Note that this code is
going to be downstreamed to the Git project. Therefore, their AI
rules[*] must be obeyed.
Equally important is that I want to be sure that you have checked and
carefully reviewed and understood everything the AI produced. I am not
going to look at the code until there is sufficient insurance that you
did. (Blatantly put, I don't want to review code produced by someone
else with AI.)
[*] https://git-scm.com/docs/SubmittingPatches#ai
-- Hannes
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] gitk: add user-defined custom commands
2026-08-05 6:59 ` Johannes Sixt
@ 2026-08-07 21:39 ` Tim Wiederhake
0 siblings, 0 replies; 4+ messages in thread
From: Tim Wiederhake @ 2026-08-07 21:39 UTC (permalink / raw)
To: Johannes Sixt; +Cc: git, Tim Wiederhake via GitGitGadget
On Wed, 2026-08-05 at 08:59 +0200, Johannes Sixt wrote:
> Am 04.08.26 um 23:43 schrieb Tim Wiederhake via GitGitGadget:
> > Allow users to define up to three custom commands each for the
> > commit
> > list and the diff display area. Commands are configured in a new
> > "Commands" tab in the preferences dialog, with a name and a command
> > template per slot. Non-empty slots appear in the right-click
> > context
> > menu of the respective area.
> >
> > Command templates support placeholder substitution (commit id,
> > commit
> > title, author name, author date, etc.) and are executed via "sh -c"
> > to allow for background execution by appending "&", and pipeing.
> > If
> > a command terminates with exit code 42, its output is displayed;
> > otherwise only non-zero exit codes are reported.
>
> Thanks, but this commit message is a bit lacking: it does not explain
> why it is a good idea to have this feature, what purpose it servers.
> For
> this reason, it is not possible to tell whether the design is sound
> and
> whether the implementation follows the design.
>
> > Signed-off-by: Tim Wiederhake <twied@gmx.net>
> > Assisted-by: Claude Opus 4.6
> Thank you for being explicit about AI assistance. Note that this code
> is
> going to be downstreamed to the Git project. Therefore, their AI
> rules[*] must be obeyed.
>
> Equally important is that I want to be sure that you have checked and
> carefully reviewed and understood everything the AI produced. I am
> not
> going to look at the code until there is sufficient insurance that
> you
> did. (Blatantly put, I don't want to review code produced by someone
> else with AI.)
>
> [*] https://git-scm.com/docs/SubmittingPatches#ai
>
> -- Hannes
Hi Hannes,
thanks for the feedback!
I agree that the commit message is a bit light on the "why" side of
things. Personally, I wrote this patch because i wanted gitk to have
the ability to let me select a line in any commit, rebase to that
commit, start an editor with that file and line selected, and after
quitting the editor amend the commit and continue the rebase. An "edit
this line at that point in history" function of sorts, because I tend
to spot typos only twenty commits later.
If I had proposed a patch to this effect, I am sure it would have been
nack'd as too niche, wrong tool, or any other reason. But having the
possibility to define custom commands allows users to adapt gitk to
their workflow instead of the other way round. I am very open to
suggestions on how to put this in the commit message without resorting
to (contrieved) examples.
Regarding the use about AI: I used Claude to produce the initial
implementation. I do not write Tcl, and frankly, this patch has not
changed that. I have reworked the code - using Claude - until it was
effectively the Tcl version of code I would have written myself in
Python or C or any other language that I actually understand. Does that
pass the bar?
Regards,
Tim
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] gitk: add user-defined custom commands
2026-08-04 21:43 [PATCH] gitk: add user-defined custom commands Tim Wiederhake via GitGitGadget
2026-08-05 6:59 ` Johannes Sixt
@ 2026-08-07 22:16 ` Junio C Hamano
1 sibling, 0 replies; 4+ messages in thread
From: Junio C Hamano @ 2026-08-07 22:16 UTC (permalink / raw)
To: Tim Wiederhake via GitGitGadget; +Cc: git, Tim Wiederhake
"Tim Wiederhake via GitGitGadget" <gitgitgadget@gmail.com> writes:
> + set len [string length $cmd_template]
> + for {set i 0} {$i < $len} {incr i} {
> + if {[string index $cmd_template $i] eq "%" && $i + 1 < $len} {
> + set next [string index $cmd_template [expr {$i + 1}]]
> + if {!$blame_computed && ($next eq "b" || $next eq "l")} {
> + set blame [get_blame_origin]
> + set blame_id [lindex $blame 0]
> + set blame_line [lindex $blame 1]
> + set blame_computed 1
> + }
> + switch -- $next {
> + "%" { append cmd "%" }
> + "i" { append cmd $id }
> + "t" { append cmd [lindex $commitinfo($id) 0] }
> + "a" { append cmd [lindex $commitinfo($id) 1] }
> + "d" { append cmd [lindex $commitinfo($id) 2] }
> + "c" { append cmd [lindex $commitinfo($id) 3] }
> + "D" { append cmd [lindex $commitinfo($id) 4] }
> + "m" { append cmd [lindex $commitinfo($id) 5] }
> + "M" { if {[info exists markedid]} { append cmd $markedid } }
> + "b" { append cmd $blame_id }
> + "f" { append cmd [get_diff_file] }
> + "l" { append cmd $blame_line }
> + default { append cmd "%" $next }
> + }
> + incr i
> + } else {
> + append cmd [string index $cmd_template $i]
> + }
> + }
> +
> + if {[catch {exec sh -c $cmd 2>@1} output]} {
What do various members of $commitinfo field have? I presume that
title and message are pretty much free text under control of anybody
who can write to the repository and entice you to run this command,
so running with "sh -c $cmd" would require $cmd to be quoting the
payload properly, or you'd be opening yourself to be an arbitrary
command execution, no? With template "echo '%t'" you thought you
are just printing the title but if the title has "title?'; echo no'" in
it, wouldn't cmd end up being
echo 'title?'; echo no''
and a more creative type can use something other than "echo no", to
have a process run under your name and do more interesting things,
right?
Note that I no longer speak Tcl (even though I admit I used to), so
if there is some "magic" that makes use of $cmd in {exec sh -c $cmd}
safe, the above may be missing the mark by a mile.
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-07 22:16 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-04 21:43 [PATCH] gitk: add user-defined custom commands Tim Wiederhake via GitGitGadget
2026-08-05 6:59 ` Johannes Sixt
2026-08-07 21:39 ` Tim Wiederhake
2026-08-07 22:16 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox