git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH] gitk: add user-defined custom commands

From
TGTim Wiederhake via GitGitGadget <gitgitgadget@gmail.com>
Date
Aug 4, 2026, 21:43 UTC
Message-ID
<pull.2371.git.git.1785879839766.gitgitgadget@gmail.com>
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
Next: Johannes Sixt
Message 1 of 9 in “gitk: add user-defined custom commands”
  1. gitk: add user-defined custom commandsTim Wiederhake via GitGitGadget, Aug 4, 2026
  2. Johannes SixtAug 5, 2026
  3. Tim WiederhakeAug 7, 2026
  4. Johannes SixtAug 9, 2026
  5. Tim WiederhakeAug 10, 2026
  6. Johannes SixtAug 11, 2026
  7. Junio C HamanoAug 7, 2026
  8. Johannes SixtAug 9, 2026
  9. Tim WiederhakeAug 10, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.