{"thread":{"id":"52154","subject":"[PATCH 0/2] git-gui: revert untracked files by deleting them","startedAt":"2019-10-30T06:48:47Z","lastAt":"2020-03-02T18:15:01Z","messageCount":57,"participants":["Jonathan Gilbert via GitGitGadget","Bert Wesarg","Jonathan Gilbert","Pratyush Yadav","Philip Oakley","Benjamin Poirier"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"385139","messageId":"pull.436.git.1572418123.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":null,"subject":"[PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-10-30T06:48:41Z","receivedAt":"2019-10-30T06:48:47Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nThis is the second revision of this change, which differs from the first\nversion in the following ways:\n\n * The change is now based on git-gui/master.\n * With one exception, all lines are at most 80 characters long. The\n   exception has a string literal in it that pushes it to 82 characters. I\n   think it would be messy to try to split it, and I got advice on \n   #git-devel to just let it go to 82 characters.\n * camelCase is eliminated. I eliminated it from existing code in a separate\n   commit.\n * try is no longer used anywhere. The code that cares about the result (had\n   code in a catch after a try) uses [catch].\n * Deletion of files and removal of empty directories is now handled by\n   separate procs.\n * The deletion of a large number of files does not block the UI during its\n   execution any more.\n * The revert_helper code no longer uses an epilogue of generic statements\n   to be evaluated on exit.\n * When deleting files, the UI is notified about the deletion directly\n   instead of doing a full rescan.\n\nJonathan Gilbert (2):\n  git-gui: consolidate naming conventions\n  git-gui: revert untracked files by deleting them\n\n lib/index.tcl | 343 ++++++++++++++++++++++++++++++++++++++------------\n 1 file changed, 266 insertions(+), 77 deletions(-)\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n-- \ngitgitgadget\n"},{"id":"385140","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1572418123.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.git.1572418123.gitgitgadget@gmail.com","subject":"[PATCH 1/2] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-10-30T06:48:42Z","receivedAt":"2019-10-30T06:48:50Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"385141","messageId":"0190f6f2f978a674a29a1e2013d00bc289851c76.1572418123.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.git.1572418123.gitgitgadget@gmail.com","subject":"[PATCH 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-10-30T06:48:43Z","receivedAt":"2019-10-30T06:48:51Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdates the revert_helper procedure to also detect untracked files. If\nfiles are present, the user is asked if they want them deleted. A new\nproc delete_files with helper delete_helper performs the deletion in\nbatches, to allow the UI to remain responsive.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 255 +++++++++++++++++++++++++++++++++++++++++++-------\n 1 file changed, 222 insertions(+), 33 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..9661ddb556 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -393,11 +393,20 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n+\t# The index is now locked. Some of the paths below include calls that\n+\t# unlock the index (e.g. checked_index). If we reach the end and the\n+\t# index is still locked, we need to unlock it before returning.\n+\tset need_unlock_index 1\n+\n \tset path_list [list]\n+\tset untracked_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n@@ -409,45 +418,225 @@ proc revert_helper {txt paths} {\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n-\t}\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n \n [mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[concat $after [list ui_ready]]\n+\n+\t\t\tset need_unlock_index 0\n+\t\t}\n+\t}\n+\n+\tif {$need_unlock_index} { unlock_index }\n+\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tdelete_files $untracked_list\n+\t\t}\n+\t}\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list} {\n+\t# Enable progress bar status updates\n+\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset deletion_error_path \"not yet captured\"\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$deletion_error_path \\\n+\t\t$batch_size\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper \\\n+\t{path_list path_index deletion_errors deletion_error_path batch_size} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors $deletion_error\n+\n+\t\t\t# Optimistically capture the path that failed, in case\n+\t\t\t# there's only one.\n+\t\t\tset deletion_error_path $path\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![lexists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$::main_status update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$deletion_error_path \\\n+\t\t\t$batch_size \\\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$::main_status stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {$deletion_error_cnt == 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"File %s could not be deleted: %s\" \\\n+\t\t\t\t$deletion_error_path \\\n+\t\t\t\t[lindex $deletion_errors 0] \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the selected files could be deleted.\" \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt]\n+\t\t}\n+\n+\t\treshow_diff\n+\t\tui_ready\n+\t}\n+}\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc lexists name {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path.\n+# If we encounter a directory that is not empty, or if a directory deletion\n+# fails, then we stop the operation and return to the caller. Even if this\n+# procedure fails to delete any directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"385148","messageId":"CAKPyHN1p+xRAF3Mjg_XqRiEUviGjj8ifP5FM=1hVb5LZUshzLw@mail.gmail.com","threadId":"52154","inReplyTo":"pull.436.git.1572418123.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Bert Wesarg","fromEmail":"bert.wesarg@googlemail.com","sentAt":"2019-10-30T09:06:26Z","receivedAt":"2019-10-30T09:06:41Z","isPatch":true,"sender":{"key":"bert.wesarg@googlemail.com","avatar":"https://avatars.githubusercontent.com/u/111934?v=4"},"body":"On Wed, Oct 30, 2019 at 7:48 AM Jonathan Gilbert via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> My development environment sometimes makes automatic changes that I don't\n> want to keep. In some cases, this involves new files being added that I\n> don't want to commit or keep (but I also don't want to outright .gitignore\n> forever). I have typically had to explicitly delete those files externally\n> to Git Gui, which is a context switch to a manual operation, and I want to\n> be able to just select those newly-created untracked files in the UI and\n> \"revert\" them into oblivion.\n\nin Git speak, that operation is called 'clean' (see 'git clean') why\nshould we overload the 'revert' operation here?\n\nBert\n\n>\n> This change updates the revert_helper proc to check for untracked files as\n> well as changes, and then changes to be reverted and untracked files are\n> handled by independent blocks of code. The user is prompted independently\n> for untracked files, since the underlying action is fundamentally different\n> (rm -f). If after deleting untracked files, the directory containing them\n> becomes empty, then the directory is removed as well. A new proc\n> delete_files takes care of actually deleting the files, using the Tcler's\n> Wiki recommended approach for keeping the UI responsive.\n>\n> This is the second revision of this change, which differs from the first\n> version in the following ways:\n>\n>  * The change is now based on git-gui/master.\n>  * With one exception, all lines are at most 80 characters long. The\n>    exception has a string literal in it that pushes it to 82 characters. I\n>    think it would be messy to try to split it, and I got advice on\n>    #git-devel to just let it go to 82 characters.\n>  * camelCase is eliminated. I eliminated it from existing code in a separate\n>    commit.\n>  * try is no longer used anywhere. The code that cares about the result (had\n>    code in a catch after a try) uses [catch].\n>  * Deletion of files and removal of empty directories is now handled by\n>    separate procs.\n>  * The deletion of a large number of files does not block the UI during its\n>    execution any more.\n>  * The revert_helper code no longer uses an epilogue of generic statements\n>    to be evaluated on exit.\n>  * When deleting files, the UI is notified about the deletion directly\n>    instead of doing a full rescan.\n>\n> Jonathan Gilbert (2):\n>   git-gui: consolidate naming conventions\n>   git-gui: revert untracked files by deleting them\n>\n>  lib/index.tcl | 343 ++++++++++++++++++++++++++++++++++++++------------\n>  1 file changed, 266 insertions(+), 77 deletions(-)\n>\n>\n> base-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v1\n> Pull-Request: https://github.com/gitgitgadget/git/pull/436\n> --\n> gitgitgadget\n"},{"id":"385164","messageId":"CAPSOpYufF8B0sBUUYqBx5YTxH+02qRom+tz4a0_2iBcNOvvMXA@mail.gmail.com","threadId":"52154","inReplyTo":"CAKPyHN1p+xRAF3Mjg_XqRiEUviGjj8ifP5FM=1hVb5LZUshzLw@mail.gmail.com","subject":"Re: [PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-10-30T17:16:04Z","receivedAt":"2019-10-30T17:16:30Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Wed, Oct 30, 2019 at 4:09 AM Bert Wesarg\nbert.wesarg-at-googlemail.com |GitHub Public/Example Allow|\n<xlwsizdz58ciy7t@sneakemail.com> wrote:\n> in Git speak, that operation is called 'clean' (see 'git clean') why\n> should we overload the 'revert' operation here?\n\nIt's less about overloading the 'revert' operation as overloading the\nUI action which is currently called \"Revert\". I think it would be a\nworse experience to have to activate a different option to remove\nunwanted files as to remove unwanted changes. Maybe the UI option\ncould be renamed \"Revert & Clean\" or something?\n\nAs a side note, `git clean untracked-file` won't do anything with a\ndefault configuration, you have to explicitly `-f` it. Not sure if\nthat's relevant, but it does feel like a higher barrier to entry than\n`git revert`.\n\nThanks,\n\nJonathan Gilbert\n"},{"id":"385329","messageId":"20191103002709.lhk3ourmj2ll6ya2@yadavpratyush.com","threadId":"52154","inReplyTo":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1572418123.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/2] git-gui: consolidate naming conventions","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-03T00:27:09Z","receivedAt":"2019-11-03T00:27:31Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nOn 30/10/19 06:48AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> A few variables in this file use camelCase, while the overall standard\n> is snake_case. A consistent naming scheme will improve readability of\n> future changes. To avoid mixing naming changes with semantic changes,\n> this commit contains only naming changes.\n\nThanks for the cleanup. Looks good.\n \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385330","messageId":"20191103011225.b6emr2ldskktokgd@yadavpratyush.com","threadId":"52154","inReplyTo":"CAPSOpYufF8B0sBUUYqBx5YTxH+02qRom+tz4a0_2iBcNOvvMXA@mail.gmail.com","subject":"Re: [PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-03T01:12:25Z","receivedAt":"2019-11-03T01:12:32Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"On 30/10/19 12:16PM, Jonathan Gilbert wrote:\n> On Wed, Oct 30, 2019 at 4:09 AM Bert Wesarg\n> bert.wesarg-at-googlemail.com |GitHub Public/Example Allow|\n> <xlwsizdz58ciy7t@sneakemail.com> wrote:\n> > in Git speak, that operation is called 'clean' (see 'git clean') why\n> > should we overload the 'revert' operation here?\n> \n> It's less about overloading the 'revert' operation as overloading the\n> UI action which is currently called \"Revert\". I think it would be a\n> worse experience to have to activate a different option to remove\n> unwanted files as to remove unwanted changes. Maybe the UI option\n> could be renamed \"Revert & Clean\" or something?\n\nI disagree. There are valid workflows where you want to remove all \nchanges to tracked files, but leave untracked ones alone. As an example, \nsay you wrote a small script to fix some textual things, like your \nvariable re-name patch. Now you run a diff before you commit those \nchanges just to be sure, and notice that your script was overzealous and \nmade some changes it shouldn't have. So, you clean up all tracked files,\nand give your script a fresh start. Here, you don't want to delete your \nscript.\n\nAnd in the other direction, say you want to delete all untracked files \nbut have unstaged changes in your tracked files. Combining \"Revert\" and \n\"Clean\" does not give you an option to only delete untracked files. So \nyou now either have to stash your changes, or run `git clean` from the \ncommand line.\n \n> As a side note, `git clean untracked-file` won't do anything with a\n> default configuration, you have to explicitly `-f` it. Not sure if\n> that's relevant, but it does feel like a higher barrier to entry than\n> `git revert`.\n\n`git revert` is different from our \"Revert\", though I admit the naming \nis quite confusing. `git revert` creates a new commit that \"reverses\" \nthe changes made in an earlier commit(s). The important point to note \nhere is that `git revert` is used when you publish some commits, and \nthen realise later they had some bugs. Now you can't just drop those \ncommits because that would re-write the history, and it would change all \nthe commit hashes since that commit. So, you use `git revert` to create \na new commit that _textually_ reverses those changes. The buggy commit \nstill exists in the tree, but its changes don't.\n\nIn contrast, git-gui's \"Revert\" works on unstaged changes. It does not \ncreate a new commit. In fact, our revert does something similar to `git \ncheckout -- <file>` (it uses `git checkout-index` to be precise).\n\nSo I don't think you should, or _can_, use `git revert` for what you \nwant to do. And so, I don't see why it is being factored in with this \ndiscussion. Am I missing something?\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385337","messageId":"CAPSOpYsBRf4tCXP-wod2h+g_=NZ-Qr0KOSZw3M=96d5FUFpFzw@mail.gmail.com","threadId":"52154","inReplyTo":"20191103011225.b6emr2ldskktokgd@yadavpratyush.com","subject":"Re: [PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-03T04:41:32Z","receivedAt":"2019-11-03T04:41:48Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sat, Nov 2, 2019, 8:12 PM Pratyush Yadav, <me@yadavpratyush.com> wrote:\n> On 30/10/19 12:16PM, Jonathan Gilbert wrote:\n> > It's less about overloading the 'revert' operation as overloading the\n> > UI action which is currently called \"Revert\". I think it would be a\n> > worse experience to have to activate a different option to remove\n> > unwanted files as to remove unwanted changes. Maybe the UI option\n> > could be renamed \"Revert & Clean\" or something?\n>\n> I disagree. There are valid workflows where you want to remove all\n> changes to tracked files, but leave untracked ones alone. As an example,\n> say you wrote a small script to fix some textual things, like your\n> variable re-name patch. Now you run a diff before you commit those\n> changes just to be sure, and notice that your script was overzealous and\n> made some changes it shouldn't have. So, you clean up all tracked files,\n> and give your script a fresh start. Here, you don't want to delete your\n> script.\n>\n> And in the other direction, say you want to delete all untracked files\n> but have unstaged changes in your tracked files. Combining \"Revert\" and\n> \"Clean\" does not give you an option to only delete untracked files. So\n> you now either have to stash your changes, or run `git clean` from the\n> command line.\n\nBut, since this is in this GUI interface, you can clearly see which\nare which and select only the files you want to affect. If you have so\nmany files that you have to select indiscriminately, then the\ncommand-line is probably a better choice anyway. In any case, my\nproposed change prompts for each part of the change, so you _can_ just\nselect everything, press ^J, and then say \"Yes\" to only one of the\nprompts.\n\n> > As a side note, `git clean untracked-file` won't do anything with a\n> > default configuration, you have to explicitly `-f` it. Not sure if\n> > that's relevant, but it does feel like a higher barrier to entry than\n> > `git revert`.\n>\n> `git revert` is different from our \"Revert\", though I admit the naming\n> is quite confusing.\n[..]\n> So I don't think you should, or _can_, use `git revert` for what you\n> want to do. And so, I don't see why it is being factored in with this\n> discussion. Am I missing something?\n\nYou are entirely correct, this was just a massive brain fart. Every\ntime I wrote `git revert` in my head I was actually thinking of\nexactly what Git Gui does, reverting working copy changes by checking\nout the file. I should have written \"reverting using `git checkout`\".\nMy apologies!\n\nIn my defence, I have over the past few days found myself digging into\ncode hosted in SVN repositories, and `svn revert` does exactly what\n`git checkout` does to an unstaged modified file. :-)\n\nJonathan Gilbert\n\nOn Sat, Nov 2, 2019 at 8:12 PM Pratyush Yadav <me@yadavpratyush.com> wrote:\n>\n> On 30/10/19 12:16PM, Jonathan Gilbert wrote:\n> > On Wed, Oct 30, 2019 at 4:09 AM Bert Wesarg\n> > bert.wesarg-at-googlemail.com |GitHub Public/Example Allow|\n> > <xlwsizdz58ciy7t@sneakemail.com> wrote:\n> > > in Git speak, that operation is called 'clean' (see 'git clean') why\n> > > should we overload the 'revert' operation here?\n> >\n> > It's less about overloading the 'revert' operation as overloading the\n> > UI action which is currently called \"Revert\". I think it would be a\n> > worse experience to have to activate a different option to remove\n> > unwanted files as to remove unwanted changes. Maybe the UI option\n> > could be renamed \"Revert & Clean\" or something?\n>\n> I disagree. There are valid workflows where you want to remove all\n> changes to tracked files, but leave untracked ones alone. As an example,\n> say you wrote a small script to fix some textual things, like your\n> variable re-name patch. Now you run a diff before you commit those\n> changes just to be sure, and notice that your script was overzealous and\n> made some changes it shouldn't have. So, you clean up all tracked files,\n> and give your script a fresh start. Here, you don't want to delete your\n> script.\n>\n> And in the other direction, say you want to delete all untracked files\n> but have unstaged changes in your tracked files. Combining \"Revert\" and\n> \"Clean\" does not give you an option to only delete untracked files. So\n> you now either have to stash your changes, or run `git clean` from the\n> command line.\n>\n> > As a side note, `git clean untracked-file` won't do anything with a\n> > default configuration, you have to explicitly `-f` it. Not sure if\n> > that's relevant, but it does feel like a higher barrier to entry than\n> > `git revert`.\n>\n> `git revert` is different from our \"Revert\", though I admit the naming\n> is quite confusing. `git revert` creates a new commit that \"reverses\"\n> the changes made in an earlier commit(s). The important point to note\n> here is that `git revert` is used when you publish some commits, and\n> then realise later they had some bugs. Now you can't just drop those\n> commits because that would re-write the history, and it would change all\n> the commit hashes since that commit. So, you use `git revert` to create\n> a new commit that _textually_ reverses those changes. The buggy commit\n> still exists in the tree, but its changes don't.\n>\n> In contrast, git-gui's \"Revert\" works on unstaged changes. It does not\n> create a new commit. In fact, our revert does something similar to `git\n> checkout -- <file>` (it uses `git checkout-index` to be precise).\n>\n> So I don't think you should, or _can_, use `git revert` for what you\n> want to do. And so, I don't see why it is being factored in with this\n> discussion. Am I missing something?\n>\n> --\n> Regards,\n> Pratyush Yadav\n"},{"id":"385338","messageId":"20191103074453.56ubga6eitjhp6rf@yadavpratyush.com","threadId":"52154","inReplyTo":"0190f6f2f978a674a29a1e2013d00bc289851c76.1572418123.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-03T07:44:53Z","receivedAt":"2019-11-03T07:45:02Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the quality re-roll. It was a pleasant read :)\n\nI would have suggested just handing off the paths to `git clean`, but it \nunfortunately does not do what we want it to do.\n\nSay we have a directory 'foo' which has one file called 'bar.txt'. That \nfile is untracked. Now, I expected `git clean -fd foo/bar.txt` to delete \n'bar.txt' _and_ 'foo/', but it only deletes bar.txt, and leaves 'foo/' \nintact. What's worse is that since 'foo' is an empty directory, it \ndoesn't appear in git-status anymore, and so there is no way the user \ncan tell the directory exists unless they go there and do a `ls`.\n\nMaybe something to fix upstream?\n\nOn 30/10/19 06:48AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Updates the revert_helper procedure to also detect untracked files. If\n\nTypo: s/Updates/Update/ ?\n\n> files are present, the user is asked if they want them deleted. A new\n> proc delete_files with helper delete_helper performs the deletion in\n> batches, to allow the UI to remain responsive.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  lib/index.tcl | 255 +++++++++++++++++++++++++++++++++++++++++++-------\n>  1 file changed, 222 insertions(+), 33 deletions(-)\n> \n> diff --git a/lib/index.tcl b/lib/index.tcl\n> index 28d4d2a54e..9661ddb556 100644\n> --- a/lib/index.tcl\n> +++ b/lib/index.tcl\n> @@ -393,11 +393,20 @@ proc revert_helper {txt paths} {\n>  \n>  \tif {![lock_index begin-update]} return\n>  \n> +\t# The index is now locked. Some of the paths below include calls that\n> +\t# unlock the index (e.g. checked_index). If we reach the end and the\n\nTypo: s/checked_index/checkout_index/\n\n> +\t# index is still locked, we need to unlock it before returning.\n> +\tset need_unlock_index 1\n> +\n>  \tset path_list [list]\n> +\tset untracked_list [list]\n>  \tset after {}\n>  \tforeach path $paths {\n>  \t\tswitch -glob -- [lindex $file_states($path) 0] {\n>  \t\tU? {continue}\n> +\t\t?O {\n> +\t\t\tlappend untracked_list $path\n> +\t\t}\n>  \t\t?M -\n>  \t\t?T -\n>  \t\t?D {\n> @@ -409,45 +418,225 @@ proc revert_helper {txt paths} {\n>  \t\t}\n>  \t}\n>  \n> +\tset path_cnt [llength $path_list]\n> +\tset untracked_cnt [llength $untracked_list]\n>  \n> -\t# Split question between singular and plural cases, because\n> -\t# such distinction is needed in some languages. Previously, the\n> -\t# code used \"Revert changes in\" for both, but that can't work\n> -\t# in languages where 'in' must be combined with word from\n> -\t# rest of string (in different way for both cases of course).\n> -\t#\n> -\t# FIXME: Unfortunately, even that isn't enough in some languages\n> -\t# as they have quite complex plural-form rules. Unfortunately,\n> -\t# msgcat doesn't seem to support that kind of string translation.\n> -\t#\n> -\tset n [llength $path_list]\n> -\tif {$n == 0} {\n> -\t\tunlock_index\n> -\t\treturn\n> -\t} elseif {$n == 1} {\n> -\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n> -\t} else {\n> -\t\tset query [mc \"Revert changes in these %i files?\" $n]\n> -\t}\n> +\tif {$path_cnt > 0} {\n> +\t\t# Split question between singular and plural cases, because\n> +\t\t# such distinction is needed in some languages. Previously, the\n> +\t\t# code used \"Revert changes in\" for both, but that can't work\n> +\t\t# in languages where 'in' must be combined with word from\n> +\t\t# rest of string (in different way for both cases of course).\n> +\t\t#\n> +\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n> +\t\t# as they have quite complex plural-form rules. Unfortunately,\n> +\t\t# msgcat doesn't seem to support that kind of string\n> +\t\t# translation.\n> +\t\t#\n> +\t\tif {$path_cnt == 1} {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Revert changes in file %s?\" \\\n> +\t\t\t\t[short_path [lindex $path_list]] \\\n> +\t\t\t\t]\n> +\t\t} else {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Revert changes in these %i files?\" \\\n> +\t\t\t\t$path_cnt]\n> +\t\t}\n>  \n> -\tset reply [tk_dialog \\\n> -\t\t.confirm_revert \\\n> -\t\t\"[appname] ([reponame])\" \\\n> -\t\t\"$query\n> +\t\tset reply [tk_dialog \\\n> +\t\t\t.confirm_revert \\\n> +\t\t\t\"[appname] ([reponame])\" \\\n> +\t\t\t\"$query\n>  \n>  [mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n> -\t\tquestion \\\n> -\t\t1 \\\n> -\t\t[mc \"Do Nothing\"] \\\n> -\t\t[mc \"Revert Changes\"] \\\n> -\t\t]\n> -\tif {$reply == 1} {\n> -\t\tcheckout_index \\\n> -\t\t\t$txt \\\n> +\t\t\tquestion \\\n> +\t\t\t1 \\\n> +\t\t\t[mc \"Do Nothing\"] \\\n> +\t\t\t[mc \"Revert Changes\"] \\\n> +\t\t\t]\n> +\n> +\t\tif {$reply == 1} {\n> +\t\t\tcheckout_index \\\n> +\t\t\t\t$txt \\\n> +\t\t\t\t$path_list \\\n> +\t\t\t\t[concat $after [list ui_ready]]\n> +\n> +\t\t\tset need_unlock_index 0\n> +\t\t}\n> +\t}\n> +\n> +\tif {$need_unlock_index} { unlock_index }\n\nAre you sure you want to unlock the index _before_ the cleanup of \nuntracked files is done? While it makes sense to unlock the index since \nour \"clean\" operation would only touch the working tree, and not the \nindex, it would also mean people can do things like \"Revert hunk\" (from \nthe context menu). Right now, this operation can not be done on \nuntracked files (so this won't be a problem for now), but I do plan on \nadding this in the future, and it wouldn't be obvious from that patch's \nPOV that this could be an issue. If someone does a \"Revert hunk\" on a \nwhile that is queued for deletion, there might be problems.\n\nAlso, would doing an `unlock_index` early allow people to run multiple \n\"clean\" jobs at the same time? Will that create race conditions that we \naren't ready to handle?\n\nIt also makes sense to evaluate what the downsides of keeping the index \nlocked are. So, does keeping the index locked prevent meaningful usage \nof git-gui, making your batched deletion pointless? Is there some reason \nfor unlocking it early that I'm missing?\n\nIf we do decide keeping the index locked is a good idea, it would be \ntroublesome to implement. `checkout_index` is asynchronous. So, when it \nreturns, the index won't necessarily be unlocked. It would get unlocked \nsome time _after_ the return. I'm not sure how to work around this.\n\n> +\n> +\tif {$untracked_cnt > 0} {\n> +\t\t# Split question between singular and plural cases, because\n> +\t\t# such distinction is needed in some languages.\n> +\t\t#\n> +\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n> +\t\t# as they have quite complex plural-form rules. Unfortunately,\n> +\t\t# msgcat doesn't seem to support that kind of string\n> +\t\t# translation.\n> +\t\t#\n> +\t\tif {$untracked_cnt == 1} {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Delete untracked file %s?\" \\\n> +\t\t\t\t[short_path [lindex $untracked_list]] \\\n> +\t\t\t\t]\n> +\t\t} else {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Delete these %i untracked files?\" \\\n> +\t\t\t\t$untracked_cnt \\\n> +\t\t\t\t]\n> +\t\t}\n> +\n> +\t\tset reply [tk_dialog \\\n> +\t\t\t.confirm_revert \\\n> +\t\t\t\"[appname] ([reponame])\" \\\n> +\t\t\t\"$query\n> +\n> +[mc \"Files will be permanently deleted.\"]\" \\\n> +\t\t\tquestion \\\n> +\t\t\t1 \\\n> +\t\t\t[mc \"Do Nothing\"] \\\n> +\t\t\t[mc \"Delete Files\"] \\\n> +\t\t\t]\n> +\n> +\t\tif {$reply == 1} {\n> +\t\t\tdelete_files $untracked_list\n> +\t\t}\n> +\t}\n> +}\n> +\n> +# Delete all of the specified files, performing deletion in batches to allow the\n> +# UI to remain responsive and updated.\n> +proc delete_files {path_list} {\n> +\t# Enable progress bar status updates\n> +\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n> +\n> +\tset path_index 0\n> +\tset deletion_errors [list]\n> +\tset deletion_error_path \"not yet captured\"\n> +\tset batch_size 50\n> +\n> +\tdelete_helper \\\n> +\t\t$path_list \\\n> +\t\t$path_index \\\n> +\t\t$deletion_errors \\\n> +\t\t$deletion_error_path \\\n> +\t\t$batch_size\n> +}\n> +\n> +# Helper function to delete a list of files in batches. Each call deletes one\n> +# batch of files, and then schedules a call for the next batch after any UI\n> +# messages have been processed.\n> +proc delete_helper \\\n> +\t{path_list path_index deletion_errors deletion_error_path batch_size} {\n> +\tglobal file_states\n> +\n> +\tset path_cnt [llength $path_list]\n> +\n> +\tset batch_remaining $batch_size\n> +\n> +\twhile {$batch_remaining > 0} {\n> +\t\tif {$path_index >= $path_cnt} { break }\n> +\n> +\t\tset path [lindex $path_list $path_index]\n> +\n> +\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n> +\n> +\t\tif {$deletion_failed} {\n> +\t\t\tlappend deletion_errors $deletion_error\n> +\n> +\t\t\t# Optimistically capture the path that failed, in case\n> +\t\t\t# there's only one.\n> +\t\t\tset deletion_error_path $path\n\nI don't see why you would do this for _only_ one path. Either do it for \nevery path. And since you're recording errors for each path, it makes \nsense to record the corresponding path too. Or, just count how many \npaths failed, and report that. I don't see why we'd want to be between \nthose two.\n\n> +\t\t} else {\n> +\t\t\tremove_empty_directories [file dirname $path]\n> +\n> +\t\t\t# Don't assume the deletion worked. Remove the file from\n> +\t\t\t# the UI, but only if it no longer exists.\n> +\t\t\tif {![lexists $path]} {\n> +\t\t\t\tunset file_states($path)\n> +\t\t\t\tdisplay_file $path __\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\tincr path_index 1\n> +\t\tincr batch_remaining -1\n> +\t}\n> +\n> +\t# Update the progress bar to indicate that this batch has been\n> +\t# completed. The update will be visible when this procedure returns\n> +\t# and allows the UI thread to process messages.\n> +\t$::main_status update $path_index $path_cnt\n> +\n> +\tif {$path_index < $path_cnt} {\n> +\t\t# The Tcler's Wiki lists this as the best practice for keeping\n> +\t\t# a UI active and processing messages during a long-running\n> +\t\t# operation.\n> +\n> +\t\tafter idle [list after 0 [list \\\n> +\t\t\tdelete_helper \\\n>  \t\t\t$path_list \\\n> -\t\t\t[concat $after [list ui_ready]]\n> +\t\t\t$path_index \\\n> +\t\t\t$deletion_errors \\\n> +\t\t\t$deletion_error_path \\\n> +\t\t\t$batch_size \\\n> +\t\t\t]]\n\nUsing `after idle` means in theory we put an undefined maximum time \nlimit on the deletion process. Though I suspect in real life it would be \na pretty short time.\n\nNonetheless, should you instead do this asynchronously, instead of \nwaiting for the event loop to enter an idle state? This means using \n`after 0` directly, instead of doing `after idle [list after 0...`. I \nhaven't tested it, but AFAIK this should also keep the UI active while \nnot depending on the state of the event loop.\n\nWhat benefits does your way have over just passing the entire list \n(without batching) to an async script to do processing in the \nbackground?\n\n>  \t} else {\n> -\t\tunlock_index\n> +\t\t# Finish the status bar operation.\n> +\t\t$::main_status stop\n> +\n> +\t\t# Report error, if any, based on how many deletions failed.\n> +\t\tset deletion_error_cnt [llength $deletion_errors]\n> +\n> +\t\tif {$deletion_error_cnt == 1} {\n> +\t\t\terror_popup [mc \\\n> +\t\t\t\t\"File %s could not be deleted: %s\" \\\n> +\t\t\t\t$deletion_error_path \\\n> +\t\t\t\t[lindex $deletion_errors 0] \\\n> +\t\t\t\t]\n> +\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n> +\t\t\terror_popup [mc \\\n> +\t\t\t\t\"None of the selected files could be deleted.\" \\\n> +\t\t\t\t]\n> +\t\t} elseif {$deletion_error_cnt > 1} {\n> +\t\t\terror_popup [mc \\\n> +\t\t\t\t\"%d of the selected files could not be deleted.\" \\\n> +\t\t\t\t$deletion_error_cnt]\n> +\t\t}\n\nThe same comment as above applies here: either show error messages for \nall paths, or for none. I don't see why you want to make a single error \npath a special case.\n\n> +\n> +\t\treshow_diff\n> +\t\tui_ready\n> +\t}\n> +}\n> +\n> +# This function is from the TCL documentation:\n> +#\n> +#   https://wiki.tcl-lang.org/page/file+exists\n\nWhy include the link? My guess is \"to give proper credit\". Do I guess \ncorrectly?\n\n> +#\n> +# [file exists] returns false if the path does exist but is a symlink to a path\n> +# that doesn't exist. This proc returns true if the path exists, regardless of\n> +# whether it is a symlink and whether it is broken.\n> +proc lexists name {\n\nNitpick: wrap the \"name\" in braces like:\n\n  proc lexists {name} {\n\nAlso, maybe re-name it to 'path_exists'? 'lexists' is not very intuitive \nunless being used _specifically_ in the context of links. Its _use_ is \nin context of paths, even though it is used to work around links.\n\n> +\texpr {![catch {file lstat $name finfo}]}\n> +}\n> +\n> +# Remove as many empty directories as we can starting at the specified path.\n\nNitpick: maybe change it to something like this?\n\n  Remove as many empty directories as we can starting at the specified \n  path, going up in the directory tree.\n\nIt was not obvious to me from reading the comment that you were going up \nthe directory tree. I thought you were going across the breadth of the \ndirectory, and was puzzled why you'd do that.\n\nBut maybe that's just me. So, I don't mind if you keep it the way it is \neither.\n\n> +# If we encounter a directory that is not empty, or if a directory deletion\n> +# fails, then we stop the operation and return to the caller. Even if this\n> +# procedure fails to delete any directories at all, it does not report failure.\n> +proc remove_empty_directories {directory_path} {\n> +\tset parent_path [file dirname $directory_path]\n> +\n> +\twhile {$parent_path != $directory_path} {\n> +\t\tset contents [glob -nocomplain -dir $directory_path *]\n> +\n> +\t\tif {[llength $contents] > 0} { break }\n> +\t\tif {[catch {file delete -- $directory_path}]} { break }\n> +\n> +\t\tset directory_path $parent_path\n> +\t\tset parent_path [file dirname $directory_path]\n>  \t}\n>  }\n\nI did some quick testing on my system, and it works fine. Thanks.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385339","messageId":"20191103074801.7fsqqi3hsefvcmby@yadavpratyush.com","threadId":"52154","inReplyTo":"CAPSOpYsBRf4tCXP-wod2h+g_=NZ-Qr0KOSZw3M=96d5FUFpFzw@mail.gmail.com","subject":"Re: [PATCH 0/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-03T07:54:58Z","receivedAt":"2019-11-03T07:55:04Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"[Dropping \"Jonathan Gilbert <logic@deltaq.org>\" from the To: list because \nmy mail server says \"Domain not found\". Putting the GGG address in To: \ninstead.]\n\nOn 02/11/19 11:41PM, Jonathan Gilbert wrote:\n> On Sat, Nov 2, 2019, 8:12 PM Pratyush Yadav, <me@yadavpratyush.com> wrote:\n> > On 30/10/19 12:16PM, Jonathan Gilbert wrote:\n> > > It's less about overloading the 'revert' operation as overloading the\n> > > UI action which is currently called \"Revert\". I think it would be a\n> > > worse experience to have to activate a different option to remove\n> > > unwanted files as to remove unwanted changes. Maybe the UI option\n> > > could be renamed \"Revert & Clean\" or something?\n> >\n> > I disagree. There are valid workflows where you want to remove all\n> > changes to tracked files, but leave untracked ones alone. As an example,\n> > say you wrote a small script to fix some textual things, like your\n> > variable re-name patch. Now you run a diff before you commit those\n> > changes just to be sure, and notice that your script was overzealous and\n> > made some changes it shouldn't have. So, you clean up all tracked files,\n> > and give your script a fresh start. Here, you don't want to delete your\n> > script.\n> >\n> > And in the other direction, say you want to delete all untracked files\n> > but have unstaged changes in your tracked files. Combining \"Revert\" and\n> > \"Clean\" does not give you an option to only delete untracked files. So\n> > you now either have to stash your changes, or run `git clean` from the\n> > command line.\n> \n> But, since this is in this GUI interface, you can clearly see which\n> are which and select only the files you want to affect. If you have so\n> many files that you have to select indiscriminately, then the\n> command-line is probably a better choice anyway. In any case, my\n> proposed change prompts for each part of the change, so you _can_ just\n> select everything, press ^J, and then say \"Yes\" to only one of the\n> prompts.\n\nAh yes! Makes sense. I got too tunnel-visioned when thinking about this, \nand lost context. Sorry.\n \n> > > As a side note, `git clean untracked-file` won't do anything with a\n> > > default configuration, you have to explicitly `-f` it. Not sure if\n> > > that's relevant, but it does feel like a higher barrier to entry than\n> > > `git revert`.\n> >\n> > `git revert` is different from our \"Revert\", though I admit the naming\n> > is quite confusing.\n> [..]\n> > So I don't think you should, or _can_, use `git revert` for what you\n> > want to do. And so, I don't see why it is being factored in with this\n> > discussion. Am I missing something?\n> \n> You are entirely correct, this was just a massive brain fart. Every\n> time I wrote `git revert` in my head I was actually thinking of\n> exactly what Git Gui does, reverting working copy changes by checking\n> out the file. I should have written \"reverting using `git checkout`\".\n> My apologies!\n> \n> In my defence, I have over the past few days found myself digging into\n> code hosted in SVN repositories, and `svn revert` does exactly what\n> `git checkout` does to an unstaged modified file. :-)\n\nI have never used svn, but I can imagine now confusing that might be ;)\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385400","messageId":"CAPSOpYsBLe+M+6xP0zdT4fY=K4poFCH_yr3-W7e_dCnG9OhhhQ@mail.gmail.com","threadId":"52154","inReplyTo":"20191103074453.56ubga6eitjhp6rf@yadavpratyush.com","subject":"Re: [PATCH 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-04T16:04:16Z","receivedAt":"2019-11-04T16:04:37Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sun, Nov 3, 2019 at 1:48 AM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> Hi Jonathan,\n>\n> Thanks for the quality re-roll. It was a pleasant read :)\n>\n> I would have suggested just handing off the paths to `git clean`, but it\n> unfortunately does not do what we want it to do.\n>\n> Say we have a directory 'foo' which has one file called 'bar.txt'. That\n> file is untracked. Now, I expected `git clean -fd foo/bar.txt` to delete\n> 'bar.txt' _and_ 'foo/', but it only deletes bar.txt, and leaves 'foo/'\n> intact. What's worse is that since 'foo' is an empty directory, it\n> doesn't appear in git-status anymore, and so there is no way the user\n> can tell the directory exists unless they go there and do a `ls`.\n>\n> Maybe something to fix upstream?\n\nPossibly, but I think the implications of such a change in the core\ntool are far greater than UI features in Git Gui.\n\n> On 30/10/19 06:48AM, Jonathan Gilbert via GitGitGadget wrote:\n> > From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> >\n> > Updates the revert_helper procedure to also detect untracked files. If\n>\n> Typo: s/Updates/Update/ ?\n\nIt wasn't a typo, I wrote it as an abbreviated form of basically \"This\nchange updates the revert_helper procedure to ...\". But, if that\nchoice of linguistic construct goes against convention I can change\nit. :-)\n\n> > +     # The index is now locked. Some of the paths below include calls that\n> > +     # unlock the index (e.g. checked_index). If we reach the end and the\n>\n> Typo: s/checked_index/checkout_index/\n\nFixed. :-)\n\n> > +     if {$need_unlock_index} { unlock_index }\n>\n> Are you sure you want to unlock the index _before_ the cleanup of\n> untracked files is done? While it makes sense to unlock the index since\n> our \"clean\" operation would only touch the working tree, and not the\n> index, it would also mean people can do things like \"Revert hunk\" (from\n> the context menu). Right now, this operation can not be done on\n> untracked files (so this won't be a problem for now), but I do plan on\n> adding this in the future, and it wouldn't be obvious from that patch's\n> POV that this could be an issue. If someone does a \"Revert hunk\" on a\n> while that is queued for deletion, there might be problems.\n>\n> Also, would doing an `unlock_index` early allow people to run multiple\n> \"clean\" jobs at the same time? Will that create race conditions that we\n> aren't ready to handle?\n>\n> It also makes sense to evaluate what the downsides of keeping the index\n> locked are. So, does keeping the index locked prevent meaningful usage\n> of git-gui, making your batched deletion pointless? Is there some reason\n> for unlocking it early that I'm missing?\n>\n> If we do decide keeping the index locked is a good idea, it would be\n> troublesome to implement. `checkout_index` is asynchronous. So, when it\n> returns, the index won't necessarily be unlocked. It would get unlocked\n> some time _after_ the return. I'm not sure how to work around this.\n\nYeah, when I wrote this I was looking at the fact that the locking of\nthe index, on the surface, only seems to interact with Git working\ncopy operations, and as you mention the fact that both tails of the\nfunction (`checkout_index` and `delete_files`) operate asynchronously\nmeans that figuring out _when_ to unlock the index is a bit tricky.\nBut, based on what you've written I understand that locking the index\nalso disables UI interaction while it's locked, and that may be\ndesirable, so we probably do want to keep it locked until both\noperations are complete.\n\nWhat we need here is something I have seen referred to as a \"chord\" --\nconceptually, a function with multiple entrypoints that get called\nfrom different threads, and then the body of the function runs only\nwhen all \"notes\" on the \"chord\" have been activated. So in this case,\nan object that has one entry-point for \"the checkout is complete\" and\none entry-point for \"the deletion is complete\". The body of the\nfunction is `unlock_index`, and then the two asynchronous functions\nboth call into their \"note\" on the \"chord\" instead of directly calling\n`unlock_index`. This would mean that the `_close_updateindex` call\nthat `checkout_index` ultimately drills down to would have to, in some\ncircumstances, _not_ unlock the index itself. I'll take a hack at this\nand see what transpires. :-)\n\n> > +             if {$deletion_failed} {\n> > +                     lappend deletion_errors $deletion_error\n> > +\n> > +                     # Optimistically capture the path that failed, in case\n> > +                     # there's only one.\n> > +                     set deletion_error_path $path\n>\n> I don't see why you would do this for _only_ one path. Either do it for\n> every path. And since you're recording errors for each path, it makes\n> sense to record the corresponding path too. Or, just count how many\n> paths failed, and report that. I don't see why we'd want to be between\n> those two.\n[..]\n> > +             } elseif {$deletion_error_cnt == $path_cnt} {\n> > +                     error_popup [mc \\\n> > +                             \"None of the selected files could be deleted.\" \\\n> > +                             ]\n> > +             } elseif {$deletion_error_cnt > 1} {\n> > +                     error_popup [mc \\\n> > +                             \"%d of the selected files could not be deleted.\" \\\n> > +                             $deletion_error_cnt]\n> > +             }\n>\n> The same comment as above applies here: either show error messages for\n> all paths, or for none. I don't see why you want to make a single error\n> path a special case.\n\nConsistency -- the prompt that asks whether you want to do a revert\n(checkout) or deletion (clean) in the first place has the same split,\nwhere if only one file matches, it identifies the file, but if\nmultiple files match, it shows the number. For consistency with that,\nI used the same logic in the error handling path.\n\n> > -                     [concat $after [list ui_ready]]\n> > +                     $path_index \\\n> > +                     $deletion_errors \\\n> > +                     $deletion_error_path \\\n> > +                     $batch_size \\\n> > +                     ]]\n>\n> Using `after idle` means in theory we put an undefined maximum time\n> limit on the deletion process. Though I suspect in real life it would be\n> a pretty short time.\n>\n> Nonetheless, should you instead do this asynchronously, instead of\n> waiting for the event loop to enter an idle state? This means using\n> `after 0` directly, instead of doing `after idle [list after 0...`. I\n> haven't tested it, but AFAIK this should also keep the UI active while\n> not depending on the state of the event loop.\n>\n> What benefits does your way have over just passing the entire list\n> (without batching) to an async script to do processing in the\n> background?\n\nI'm not familiar with async scripts, I'm pretty new to Tcl. Is that\nbasically a mechanism like threads? I wrote the batching simply\nbecause doing the call synchronously meant that if thousands of files\nwere selected, the UI would freeze hard for several seconds and that\nseemed like a bad experience. If there's a better way to delete\nthousands of files while keeping the UI responsive and providing\nfeedback, that'd make more sense, but I don't know how to do it :-)\n\n> > +# This function is from the TCL documentation:\n> > +#\n> > +#   https://wiki.tcl-lang.org/page/file+exists\n>\n> Why include the link? My guess is \"to give proper credit\". Do I guess\n> correctly?\n\nActually it's more to say, \"If you're reading through this code and\nthe specific nuances of this procedure aren't obvious, here's the\nprocedure's origin. I believe it to be a reliable source, so if that's\ngood enough for you too, then you don't need to concern yourself with\nthe implementation details, you can just trust that somebody else put\ntime into the definition of this above and beyond the scope of the\nchange where I'm using it.\" :-)\n\n> > +# [file exists] returns false if the path does exist but is a symlink to a path\n> > +# that doesn't exist. This proc returns true if the path exists, regardless of\n> > +# whether it is a symlink and whether it is broken.\n> > +proc lexists name {\n>\n> Nitpick: wrap the \"name\" in braces like:\n>\n>   proc lexists {name} {\n>\n> Also, maybe re-name it to 'path_exists'? 'lexists' is not very intuitive\n> unless being used _specifically_ in the context of links. Its _use_ is\n> in context of paths, even though it is used to work around links.\n\nI can make those changes. I had initially copy/pasted it with no\nchanges at all, so that, in the context of the preceding explanation,\na future reader could easily verify that, \"Yes, this really is exactly\nthe same procedure definition.\" :-)\n\n> > +# Remove as many empty directories as we can starting at the specified path.\n>\n> Nitpick: maybe change it to something like this?\n>\n>   Remove as many empty directories as we can starting at the specified\n>   path, going up in the directory tree.\n>\n> It was not obvious to me from reading the comment that you were going up\n> the directory tree. I thought you were going across the breadth of the\n> directory, and was puzzled why you'd do that.\n>\n> But maybe that's just me. So, I don't mind if you keep it the way it is\n> either.\n\nThat's legitimate :-) I knew exactly what the function did _before_ I\nwrote the comment, after all.\n\nLet me know about those few things, and I'll send in another iteration:\n\n* Is it preferable to use imperative rather than third person singular\nin commit messages? (\"[I] make the change\" vs. \"[It] makes the\nchange\")\n* Should I simplify the error messages, rather than having parity with\nthe prompts w.r.t. one vs. multiple items?\n* Async scripts for longer background operations, rather than batching\non the UI thread? Can they post ongoing status updates too?\n* Should I modify `lexists` to fit the file's conventions, or keep it\nan exact copy/paste from the external source?\n\nThanks very much,\n\nJonathan Gilbert\n"},{"id":"385405","messageId":"CAPSOpYtBRvDX4by_k_ViuFN66vKx=WM7zcQ_JXivxApDBOGJ1w@mail.gmail.com","threadId":"52154","inReplyTo":"20191103074453.56ubga6eitjhp6rf@yadavpratyush.com","subject":"Re: [PATCH 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-04T17:36:07Z","receivedAt":"2019-11-04T17:36:23Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sun, Nov 3, 2019 at 1:48 AM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> > +             after idle [list after 0 [list \\\n> > +                     delete_helper \\\n> >                       $path_list \\\n> > -                     [concat $after [list ui_ready]]\n> > +                     $path_index \\\n> > +                     $deletion_errors \\\n> > +                     $deletion_error_path \\\n> > +                     $batch_size \\\n> > +                     ]]\n>\n> Using `after idle` means in theory we put an undefined maximum time\n> limit on the deletion process. Though I suspect in real life it would be\n> a pretty short time.\n>\n> Nonetheless, should you instead do this asynchronously, instead of\n> waiting for the event loop to enter an idle state? This means using\n> `after 0` directly, instead of doing `after idle [list after 0...`. I\n> haven't tested it, but AFAIK this should also keep the UI active while\n> not depending on the state of the event loop.\n>\n> What benefits does your way have over just passing the entire list\n> (without batching) to an async script to do processing in the\n> background?\n\nI forgot to include this in my point-form list at the end of the\npreceding e-mail. What should I be looking into to achieve the same\nsort of behaviour, where the UI isn't frozen and the user is getting\nperiod updates about the progress of a large deletion, without using\nbatches on the UI thread? Is that a thing, or am I misunderstanding\nyou w.r.t. to doing this asynchronously?\n\nFor what it's worth, I used `after idle {after 0 ..}` based on the\nrecommendation of the Tcler's Wiki [0]:\n\n> An after idle that reschedules itself causes trouble, as the manual warns (PYK 2012-09: the docs no-longer contain this warning, but it still applies):\n>\n>      At present it is not safe for an idle callback to reschedule itself\n>      continuously.  This will interact badly with certain features of\n>      Tk that attempt to wait for all idle callbacks to complete.\n>      If you would like for an idle callback to reschedule itself\n>      continuously, it is better to use a timer handler with a zero\n>      timeout period.\n>\n> Even this warning is oversimplified. Simply scheduling a timer handler with a zero timeout period can mean that the event loop will never be idle, keeping other idle callbacks from firing. The truly safe approach combines both:\n>\n>     proc doOneStep {} {\n>      if { [::sim::one_step] } {\n>          after idle [list after 0 doOneStep]\n>          #this would work just as well:\n>          #after 0 [list after idle doOneStep]\n>      }\n>      return\n>     }\n>     sim::init .c 640 480\n>     doOneStep\n>\n> This skeleton should be considered the basic framework for performing long running calculations within a single Tcl interpreter.\n\nThanks,\n\nJonathan Gilbert\n\n[0] https://wiki.tcl-lang.org/page/Keep+a+GUI+alive+during+a+long+calculation\n"},{"id":"385674","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1573110335.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v2.git.1573110335.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-07T07:05:34Z","receivedAt":"2019-11-07T07:05:42Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"385675","messageId":"9469beb59937f87647190cf7f56544b8c27e20b6.1573110335.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v2.git.1573110335.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-07T07:05:35Z","receivedAt":"2019-11-07T07:05:43Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper procedure to also detect untracked files. If\nfiles are present, the user is asked if they want them deleted. Perform\nthe deletion in batches, using new proc delete_files with helper\ndelete_helper, to allow the UI to remain responsive. Coordinate the\ncompletion of multiple overlapping asynchronous operations using a new\nconstruct called a \"chord\". Migrate unlocking of the index out of\n_close_updateindex to a responsibility of the caller, to permit paths\nthat don't directly unlock the index.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/chord.tcl | 137 ++++++++++++++++++++++++\n lib/index.tcl | 288 +++++++++++++++++++++++++++++++++++++++++++-------\n 2 files changed, 385 insertions(+), 40 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..2d13af14fc\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,137 @@\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+#\n+#   Constructor:\n+#     set chord [SimpleChord new {body}]\n+#       Creates a new chord object with the specified body script. The body\n+#       script is evaluated at most once, when a note is activated and the\n+#       chord has no other non-activated notes.\n+#\n+#   Methods:\n+#     $chord eval {script}\n+#       Runs the specified script in the same context (namespace) in which the\n+#       chord body will be evaluated. This can be used to set variable values\n+#       for the chord body to use.\n+#\n+#     set note [$chord add_note]\n+#       Adds a new note to the chord, an instance of ChordNote. Raises an\n+#       error if the chord is already completed, otherwise the chord is updated\n+#       so that the new note must also be activated before the body is\n+#       evaluated.\n+#\n+#     $chord notify_note_activation\n+#       For internal use only.\n+#\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+#\n+#   Constructor:\n+#     Instances of ChordNote are created internally by calling add_note on\n+#     SimpleChord objects.\n+#\n+#   Methods:\n+#     [$note is_activated]\n+#       Returns true if this note has already been activated.\n+#\n+#     $note\n+#       Activates the note, if it has not already been activated, and completes\n+#       the chord if there are no other notes awaiting activation. Subsequent\n+#       calls will have no further effect.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+oo::class create SimpleChord {\n+\tvariable Notes\n+\tvariable Body\n+\tvariable IsCompleted\n+\n+\tconstructor {body} {\n+\t\tset Notes [list]\n+\t\tset Body $body\n+\t\tset IsCompleted 0\n+\t}\n+\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\tmethod add_note {} {\n+\t\tif {$IsCompleted} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend Notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\tmethod notify_note_activation {} {\n+\t\tif {!$IsCompleted} {\n+\t\t\tforeach note $Notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset IsCompleted 1\n+\n+\t\t\tnamespace eval [self] $Body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+oo::class create ChordNote {\n+\tvariable Chord IsActivated\n+\n+\tconstructor {chord} {\n+\t\tset Chord $chord\n+\t\tset IsActivated 0\n+\t}\n+\n+\tmethod is_activated {} {\n+\t\treturn $IsActivated\n+\t}\n+\n+\tmethod unknown {} {\n+\t\tif {!$IsActivated} {\n+\t\t\tset IsActivated 1\n+\t\t\t$Chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..64046d6833 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,7 +7,7 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n+proc _close_updateindex {fd} {\n \tglobal use_ttk NS\n \tfconfigure $fd -blocking 1\n \tif {[catch {close $fd} err]} {\n@@ -52,8 +52,6 @@ proc _close_updateindex {fd after} {\n \t}\n \n \t$::main_status stop\n-\tunlock_index\n-\tuplevel #0 $after\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -90,7 +88,9 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t_close_updateindex $fd\n+\t\tunlock_index\n+\t\tuplevel #0 $after\n \t\treturn\n \t}\n \n@@ -156,7 +156,9 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t_close_updateindex $fd\n+\t\tunlock_index\n+\t\tuplevel #0 $after\n \t\treturn\n \t}\n \n@@ -233,7 +235,8 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t_close_updateindex $fd $do_unlock_index $after\n+\t\tuplevel #0 $after\n \t\treturn\n \t}\n \n@@ -393,61 +396,266 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\tset after_chord [SimpleChord new {\n+\t\tunlock_index\n+\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\tui_ready\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n+\tset untracked_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n-\t}\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n \n [mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$::main_status update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$::main_status stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text \"Encountered errors deleting files:\\n\"\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"385676","messageId":"pull.436.v2.git.1573110335.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.git.1572418123.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-07T07:05:33Z","receivedAt":"2019-11-07T07:05:44Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nThis is the third revision of this change, which differs from the second\nversion in the following ways:\n\n * A new construct called a \"chord\" is used to coordinate the completion of\n   multiple asynchronous operations that can be kicked off by revert_helper.\n   A chord is, conceptually, a procedure with multiple entrypoints whose\n   body only executes once all entrypoints have been activated. The \n   chord.tcl file includes comprehensive documentation of how to use the\n   chord classes.\n   \n   \n * Since we might not yet be ready to unlock the index when checkout_index \n   returns, the _close_updateindex proc where it was ultimately unlocking\n   the index has been modified so that unlocking the index is the\n   responsibility of the caller. Since the $after functionality ran after \n   unlock_index, that is also hoisted out. Nothing in _close_updateindex \n   appears to be asynchronous, so the caller can simply make the calls\n   itself upon its return.\n   \n   \n * lexists has been renamed to path_exists.\n   \n   \n * Up to 10 deletion errors are now shown simultaneously. I also confirmed\n   that Tcl's file delete code will always return a nicely-formatted error\n   including the filename, and changed the message so that it isn't also \n   injecting the filename.\n\nJonathan Gilbert (2):\n  git-gui: consolidate naming conventions\n  git-gui: revert untracked files by deleting them\n\n lib/chord.tcl | 137 ++++++++++++++++++\n lib/index.tcl | 376 +++++++++++++++++++++++++++++++++++++++-----------\n 2 files changed, 429 insertions(+), 84 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v1:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n 2:  0190f6f2f9 ! 2:  9469beb599 git-gui: revert untracked files by deleting them\n     @@ -2,24 +2,233 @@\n      \n          git-gui: revert untracked files by deleting them\n      \n     -    Updates the revert_helper procedure to also detect untracked files. If\n     -    files are present, the user is asked if they want them deleted. A new\n     -    proc delete_files with helper delete_helper performs the deletion in\n     -    batches, to allow the UI to remain responsive.\n     +    Update the revert_helper procedure to also detect untracked files. If\n     +    files are present, the user is asked if they want them deleted. Perform\n     +    the deletion in batches, using new proc delete_files with helper\n     +    delete_helper, to allow the UI to remain responsive. Coordinate the\n     +    completion of multiple overlapping asynchronous operations using a new\n     +    construct called a \"chord\". Migrate unlocking of the index out of\n     +    _close_updateindex to a responsibility of the caller, to permit paths\n     +    that don't directly unlock the index.\n      \n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n     + diff --git a/lib/chord.tcl b/lib/chord.tcl\n     + new file mode 100644\n     + --- /dev/null\n     + +++ b/lib/chord.tcl\n     +@@\n     ++# SimpleChord class:\n     ++#   Represents a procedure that conceptually has multiple entrypoints that must\n     ++#   all be called before the procedure executes. Each entrypoint is called a\n     ++#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n     ++#\n     ++#   Constructor:\n     ++#     set chord [SimpleChord new {body}]\n     ++#       Creates a new chord object with the specified body script. The body\n     ++#       script is evaluated at most once, when a note is activated and the\n     ++#       chord has no other non-activated notes.\n     ++#\n     ++#   Methods:\n     ++#     $chord eval {script}\n     ++#       Runs the specified script in the same context (namespace) in which the\n     ++#       chord body will be evaluated. This can be used to set variable values\n     ++#       for the chord body to use.\n     ++#\n     ++#     set note [$chord add_note]\n     ++#       Adds a new note to the chord, an instance of ChordNote. Raises an\n     ++#       error if the chord is already completed, otherwise the chord is updated\n     ++#       so that the new note must also be activated before the body is\n     ++#       evaluated.\n     ++#\n     ++#     $chord notify_note_activation\n     ++#       For internal use only.\n     ++#\n     ++# ChordNote class:\n     ++#   Represents a note within a chord, providing a way to activate it. When the\n     ++#   final note of the chord is activated (this can be any note in the chord,\n     ++#   with all other notes already previously activated in any order), the chord's\n     ++#   body is evaluated.\n     ++#\n     ++#   Constructor:\n     ++#     Instances of ChordNote are created internally by calling add_note on\n     ++#     SimpleChord objects.\n     ++#\n     ++#   Methods:\n     ++#     [$note is_activated]\n     ++#       Returns true if this note has already been activated.\n     ++#\n     ++#     $note\n     ++#       Activates the note, if it has not already been activated, and completes\n     ++#       the chord if there are no other notes awaiting activation. Subsequent\n     ++#       calls will have no further effect.\n     ++#\n     ++# Example:\n     ++#\n     ++#   # Turn off the UI while running a couple of async operations.\n     ++#   lock_ui\n     ++#\n     ++#   set chord [SimpleChord new {\n     ++#     unlock_ui\n     ++#     # Note: $notice here is not referenced in the calling scope\n     ++#     if {$notice} { info_popup $notice }\n     ++#   }\n     ++#\n     ++#   # Configure a note to keep the chord from completing until\n     ++#   # all operations have been initiated.\n     ++#   set common_note [$chord add_note]\n     ++#\n     ++#   # Pass notes as 'after' callbacks to other operations\n     ++#   async_operation $args [$chord add_note]\n     ++#   other_async_operation $args [$chord add_note]\n     ++#\n     ++#   # Communicate with the chord body\n     ++#   if {$condition} {\n     ++#     # This sets $notice in the same context that the chord body runs in.\n     ++#     $chord eval { set notice \"Something interesting\" }\n     ++#   }\n     ++#\n     ++#   # Activate the common note, making the chord eligible to complete\n     ++#   $common_note\n     ++#\n     ++# At this point, the chord will complete at some unknown point in the future.\n     ++# The common note might have been the first note activated, or the async\n     ++# operations might have completed synchronously and the common note is the\n     ++# last one, completing the chord before this code finishes, or anything in\n     ++# between. The purpose of the chord is to not have to worry about the order.\n     ++\n     ++oo::class create SimpleChord {\n     ++\tvariable Notes\n     ++\tvariable Body\n     ++\tvariable IsCompleted\n     ++\n     ++\tconstructor {body} {\n     ++\t\tset Notes [list]\n     ++\t\tset Body $body\n     ++\t\tset IsCompleted 0\n     ++\t}\n     ++\n     ++\tmethod eval {script} {\n     ++\t\tnamespace eval [self] $script\n     ++\t}\n     ++\n     ++\tmethod add_note {} {\n     ++\t\tif {$IsCompleted} { error \"Cannot add a note to a completed chord\" }\n     ++\n     ++\t\tset note [ChordNote new [self]]\n     ++\n     ++\t\tlappend Notes $note\n     ++\n     ++\t\treturn $note\n     ++\t}\n     ++\n     ++\tmethod notify_note_activation {} {\n     ++\t\tif {!$IsCompleted} {\n     ++\t\t\tforeach note $Notes {\n     ++\t\t\t\tif {![$note is_activated]} { return }\n     ++\t\t\t}\n     ++\n     ++\t\t\tset IsCompleted 1\n     ++\n     ++\t\t\tnamespace eval [self] $Body\n     ++\t\t\tnamespace delete [self]\n     ++\t\t}\n     ++\t}\n     ++}\n     ++\n     ++oo::class create ChordNote {\n     ++\tvariable Chord IsActivated\n     ++\n     ++\tconstructor {chord} {\n     ++\t\tset Chord $chord\n     ++\t\tset IsActivated 0\n     ++\t}\n     ++\n     ++\tmethod is_activated {} {\n     ++\t\treturn $IsActivated\n     ++\t}\n     ++\n     ++\tmethod unknown {} {\n     ++\t\tif {!$IsActivated} {\n     ++\t\t\tset IsActivated 1\n     ++\t\t\t$Chord notify_note_activation\n     ++\t\t}\n     ++\t}\n     ++}\n     +\n       diff --git a/lib/index.tcl b/lib/index.tcl\n       --- a/lib/index.tcl\n       +++ b/lib/index.tcl\n     +@@\n     + \t}\n     + }\n     + \n     +-proc _close_updateindex {fd after} {\n     ++proc _close_updateindex {fd} {\n     + \tglobal use_ttk NS\n     + \tfconfigure $fd -blocking 1\n     + \tif {[catch {close $fd} err]} {\n     +@@\n     + \t}\n     + \n     + \t$::main_status stop\n     +-\tunlock_index\n     +-\tuplevel #0 $after\n     + }\n     + \n     + proc update_indexinfo {msg path_list after} {\n     +@@\n     + \tglobal file_states current_diff_path\n     + \n     + \tif {$update_index_cp >= $total_cnt} {\n     +-\t\t_close_updateindex $fd $after\n     ++\t\t_close_updateindex $fd\n     ++\t\tunlock_index\n     ++\t\tuplevel #0 $after\n     + \t\treturn\n     + \t}\n     + \n     +@@\n     + \tglobal file_states current_diff_path\n     + \n     + \tif {$update_index_cp >= $total_cnt} {\n     +-\t\t_close_updateindex $fd $after\n     ++\t\t_close_updateindex $fd\n     ++\t\tunlock_index\n     ++\t\tuplevel #0 $after\n     + \t\treturn\n     + \t}\n     + \n     +@@\n     + \tglobal file_states current_diff_path\n     + \n     + \tif {$update_index_cp >= $total_cnt} {\n     +-\t\t_close_updateindex $fd $after\n     ++\t\t_close_updateindex $fd $do_unlock_index $after\n     ++\t\tuplevel #0 $after\n     + \t\treturn\n     + \t}\n     + \n      @@\n       \n       \tif {![lock_index begin-update]} return\n       \n     -+\t# The index is now locked. Some of the paths below include calls that\n     -+\t# unlock the index (e.g. checked_index). If we reach the end and the\n     -+\t# index is still locked, we need to unlock it before returning.\n     -+\tset need_unlock_index 1\n     ++\t# Common \"after\" functionality that waits until multiple asynchronous\n     ++\t# operations are complete (by waiting for them to activate their notes\n     ++\t# on the chord).\n     ++\tset after_chord [SimpleChord new {\n     ++\t\tunlock_index\n     ++\t\tif {$should_reshow_diff} { reshow_diff }\n     ++\t\tui_ready\n     ++\t}]\n     ++\n     ++\t$after_chord eval { set should_reshow_diff 0 }\n     ++\n     ++\t# We don't know how many notes we're going to create (it's dynamic based\n     ++\t# on conditional paths below), so create a common note that will delay\n     ++\t# the chord's completion until we activate it, and then activate it\n     ++\t# after all the other notes have been created.\n     ++\tset after_common_note [$after_chord add_note]\n      +\n       \tset path_list [list]\n      +\tset untracked_list [list]\n     @@ -33,7 +242,12 @@\n       \t\t?M -\n       \t\t?T -\n       \t\t?D {\n     -@@\n     + \t\t\tlappend path_list $path\n     + \t\t\tif {$path eq $current_diff_path} {\n     +-\t\t\t\tset after {reshow_diff;}\n     ++\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n     + \t\t\t}\n     + \t\t}\n       \t\t}\n       \t}\n       \n     @@ -110,14 +324,10 @@\n      +\t\t\tcheckout_index \\\n      +\t\t\t\t$txt \\\n      +\t\t\t\t$path_list \\\n     -+\t\t\t\t[concat $after [list ui_ready]]\n     -+\n     -+\t\t\tset need_unlock_index 0\n     ++\t\t\t\t[$after_chord add_note]\n      +\t\t}\n      +\t}\n      +\n     -+\tif {$need_unlock_index} { unlock_index }\n     -+\n      +\tif {$untracked_cnt > 0} {\n      +\t\t# Split question between singular and plural cases, because\n      +\t\t# such distinction is needed in some languages.\n     @@ -152,35 +362,40 @@\n      +\t\t\t]\n      +\n      +\t\tif {$reply == 1} {\n     -+\t\t\tdelete_files $untracked_list\n     ++\t\t\t$after_chord eval { set should_reshow_diff 1 }\n     ++\n     ++\t\t\tdelete_files $untracked_list [$after_chord add_note]\n      +\t\t}\n      +\t}\n     ++\n     ++\t# Activate the common note. If no other notes were created, this\n     ++\t# completes the chord. If other notes were created, then this common\n     ++\t# note prevents a race condition where the chord might complete early.\n     ++\t$after_common_note\n      +}\n      +\n      +# Delete all of the specified files, performing deletion in batches to allow the\n      +# UI to remain responsive and updated.\n     -+proc delete_files {path_list} {\n     ++proc delete_files {path_list after} {\n      +\t# Enable progress bar status updates\n      +\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n      +\n      +\tset path_index 0\n      +\tset deletion_errors [list]\n     -+\tset deletion_error_path \"not yet captured\"\n      +\tset batch_size 50\n      +\n      +\tdelete_helper \\\n      +\t\t$path_list \\\n      +\t\t$path_index \\\n      +\t\t$deletion_errors \\\n     -+\t\t$deletion_error_path \\\n     -+\t\t$batch_size\n     ++\t\t$batch_size \\\n     ++\t\t$after\n      +}\n      +\n      +# Helper function to delete a list of files in batches. Each call deletes one\n      +# batch of files, and then schedules a call for the next batch after any UI\n      +# messages have been processed.\n     -+proc delete_helper \\\n     -+\t{path_list path_index deletion_errors deletion_error_path batch_size} {\n     ++proc delete_helper {path_list path_index deletion_errors batch_size after} {\n      +\tglobal file_states\n      +\n      +\tset path_cnt [llength $path_list]\n     @@ -195,17 +410,13 @@\n      +\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n      +\n      +\t\tif {$deletion_failed} {\n     -+\t\t\tlappend deletion_errors $deletion_error\n     -+\n     -+\t\t\t# Optimistically capture the path that failed, in case\n     -+\t\t\t# there's only one.\n     -+\t\t\tset deletion_error_path $path\n     ++\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n      +\t\t} else {\n      +\t\t\tremove_empty_directories [file dirname $path]\n      +\n      +\t\t\t# Don't assume the deletion worked. Remove the file from\n      +\t\t\t# the UI, but only if it no longer exists.\n     -+\t\t\tif {![lexists $path]} {\n     ++\t\t\tif {![path_exists $path]} {\n      +\t\t\t\tunset file_states($path)\n      +\t\t\t\tdisplay_file $path __\n      +\t\t\t}\n     @@ -231,8 +442,8 @@\n      -\t\t\t[concat $after [list ui_ready]]\n      +\t\t\t$path_index \\\n      +\t\t\t$deletion_errors \\\n     -+\t\t\t$deletion_error_path \\\n      +\t\t\t$batch_size \\\n     ++\t\t\t$after\n      +\t\t\t]]\n       \t} else {\n      -\t\tunlock_index\n     @@ -242,27 +453,33 @@\n      +\t\t# Report error, if any, based on how many deletions failed.\n      +\t\tset deletion_error_cnt [llength $deletion_errors]\n      +\n     -+\t\tif {$deletion_error_cnt == 1} {\n     -+\t\t\terror_popup [mc \\\n     -+\t\t\t\t\"File %s could not be deleted: %s\" \\\n     -+\t\t\t\t$deletion_error_path \\\n     -+\t\t\t\t[lindex $deletion_errors 0] \\\n     -+\t\t\t\t]\n     ++\t\tif {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n     ++\t\t\tset error_text \"Encountered errors deleting files:\\n\"\n     ++\n     ++\t\t\tforeach deletion_error $deletion_errors {\n     ++\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n     ++\t\t\t}\n     ++\n     ++\t\t\terror_popup $error_text\n      +\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n      +\t\t\terror_popup [mc \\\n     -+\t\t\t\t\"None of the selected files could be deleted.\" \\\n     ++\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n     ++\t\t\t\t$path_cnt \\\n      +\t\t\t\t]\n      +\t\t} elseif {$deletion_error_cnt > 1} {\n      +\t\t\terror_popup [mc \\\n     -+\t\t\t\t\"%d of the selected files could not be deleted.\" \\\n     -+\t\t\t\t$deletion_error_cnt]\n     ++\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n     ++\t\t\t\t$deletion_error_cnt \\\n     ++\t\t\t\t$path_cnt \\\n     ++\t\t\t\t]\n      +\t\t}\n      +\n     -+\t\treshow_diff\n     -+\t\tui_ready\n     ++\t\tuplevel #0 $after\n      +\t}\n      +}\n      +\n     ++proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n     ++\n      +# This function is from the TCL documentation:\n      +#\n      +#   https://wiki.tcl-lang.org/page/file+exists\n     @@ -270,14 +487,15 @@\n      +# [file exists] returns false if the path does exist but is a symlink to a path\n      +# that doesn't exist. This proc returns true if the path exists, regardless of\n      +# whether it is a symlink and whether it is broken.\n     -+proc lexists name {\n     ++proc path_exists {name} {\n      +\texpr {![catch {file lstat $name finfo}]}\n      +}\n      +\n     -+# Remove as many empty directories as we can starting at the specified path.\n     -+# If we encounter a directory that is not empty, or if a directory deletion\n     -+# fails, then we stop the operation and return to the caller. Even if this\n     -+# procedure fails to delete any directories at all, it does not report failure.\n     ++# Remove as many empty directories as we can starting at the specified path,\n     ++# walking up the directory tree. If we encounter a directory that is not\n     ++# empty, or if a directory deletion fails, then we stop the operation and\n     ++# return to the caller. Even if this procedure fails to delete any\n     ++# directories at all, it does not report failure.\n      +proc remove_empty_directories {directory_path} {\n      +\tset parent_path [file dirname $directory_path]\n      +\n\n-- \ngitgitgadget\n"},{"id":"385922","messageId":"20191111192526.crllpe3phitneu3p@yadavpratyush.com","threadId":"52154","inReplyTo":"9469beb59937f87647190cf7f56544b8c27e20b6.1573110335.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-11T19:25:26Z","receivedAt":"2019-11-11T19:25:35Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the re-roll. Some comments below. Apart from those comments, \nthis looks close to good enough for merging :)\n\nOn 07/11/19 07:05AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the revert_helper procedure to also detect untracked files. If\n> files are present, the user is asked if they want them deleted. Perform\n> the deletion in batches, using new proc delete_files with helper\n> delete_helper, to allow the UI to remain responsive. Coordinate the\n> completion of multiple overlapping asynchronous operations using a new\n> construct called a \"chord\". Migrate unlocking of the index out of\n> _close_updateindex to a responsibility of the caller, to permit paths\n> that don't directly unlock the index.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  lib/chord.tcl | 137 ++++++++++++++++++++++++\n>  lib/index.tcl | 288 +++++++++++++++++++++++++++++++++++++++++++-------\n>  2 files changed, 385 insertions(+), 40 deletions(-)\n>  create mode 100644 lib/chord.tcl\n> \n> diff --git a/lib/chord.tcl b/lib/chord.tcl\n> new file mode 100644\n> index 0000000000..2d13af14fc\n> --- /dev/null\n> +++ b/lib/chord.tcl\n> @@ -0,0 +1,137 @@\n\nThe 'class' documentation [0] suggests adding a \"package require TclOO\". \nBut TclOO ships by default with Tcl 8.6 and above. So, I'm not really \nsure if we need this.\n\n> +# SimpleChord class:\n> +#   Represents a procedure that conceptually has multiple entrypoints that must\n> +#   all be called before the procedure executes. Each entrypoint is called a\n> +#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n> +#\n> +#   Constructor:\n> +#     set chord [SimpleChord new {body}]\n> +#       Creates a new chord object with the specified body script. The body\n> +#       script is evaluated at most once, when a note is activated and the\n> +#       chord has no other non-activated notes.\n> +#\n> +#   Methods:\n> +#     $chord eval {script}\n> +#       Runs the specified script in the same context (namespace) in which the\n> +#       chord body will be evaluated. This can be used to set variable values\n> +#       for the chord body to use.\n> +#\n> +#     set note [$chord add_note]\n> +#       Adds a new note to the chord, an instance of ChordNote. Raises an\n> +#       error if the chord is already completed, otherwise the chord is updated\n> +#       so that the new note must also be activated before the body is\n> +#       evaluated.\n> +#\n> +#     $chord notify_note_activation\n> +#       For internal use only.\n> +#\n> +# ChordNote class:\n> +#   Represents a note within a chord, providing a way to activate it. When the\n> +#   final note of the chord is activated (this can be any note in the chord,\n> +#   with all other notes already previously activated in any order), the chord's\n> +#   body is evaluated.\n> +#\n> +#   Constructor:\n> +#     Instances of ChordNote are created internally by calling add_note on\n> +#     SimpleChord objects.\n> +#\n> +#   Methods:\n> +#     [$note is_activated]\n> +#       Returns true if this note has already been activated.\n> +#\n> +#     $note\n> +#       Activates the note, if it has not already been activated, and completes\n> +#       the chord if there are no other notes awaiting activation. Subsequent\n> +#       calls will have no further effect.\n\nNice to see some good documentation!\n\nOne nitpick: would it make more sense to have the documentation for a \nmethod/constructor just above that method/constructor? This way, when \nsomeone updates the code some time later, they'll also hopefully \nremember to update the documentation. It is much more likely to be stale \nif all of it just stays on the top.\n\n> +#\n> +# Example:\n> +#\n> +#   # Turn off the UI while running a couple of async operations.\n> +#   lock_ui\n> +#\n> +#   set chord [SimpleChord new {\n> +#     unlock_ui\n> +#     # Note: $notice here is not referenced in the calling scope\n> +#     if {$notice} { info_popup $notice }\n> +#   }\n> +#\n> +#   # Configure a note to keep the chord from completing until\n> +#   # all operations have been initiated.\n> +#   set common_note [$chord add_note]\n> +#\n> +#   # Pass notes as 'after' callbacks to other operations\n> +#   async_operation $args [$chord add_note]\n> +#   other_async_operation $args [$chord add_note]\n> +#\n> +#   # Communicate with the chord body\n> +#   if {$condition} {\n> +#     # This sets $notice in the same context that the chord body runs in.\n> +#     $chord eval { set notice \"Something interesting\" }\n> +#   }\n> +#\n> +#   # Activate the common note, making the chord eligible to complete\n> +#   $common_note\n> +#\n> +# At this point, the chord will complete at some unknown point in the future.\n> +# The common note might have been the first note activated, or the async\n> +# operations might have completed synchronously and the common note is the\n> +# last one, completing the chord before this code finishes, or anything in\n> +# between. The purpose of the chord is to not have to worry about the order.\n> +\n> +oo::class create SimpleChord {\n\nThis comes from the TclOO package, right?\n\ngit-gui has its own object-oriented system (lib/class.tcl). It was \nwritten circa 2007. I suspect something like TclOO did not exist back \nthen.\n\nWhy not use that? Does it have some limitations that TclOO does not \nhave? I do not mind using the \"official\" OO system. I just want to know \nwhy exactly you made the choice.\n\nWe would end up mixing the two implementations/flavors in the same \ncodebase, but as long as they don't interfere with each other and are \ncross compatible (which I think they are, but I haven't tested), I don't \nmind some \"modernization\" of the codebase. \n\nMore importantly, TclOO ships as part of the core distribution with Tcl \n8.6, but as of now the minimum version required for git-gui is 8.4. So, \nI think we should bump the minimum version (8.6 released circa 2012, so \nmost people should have caught up by now I hope).\n\n> +\tvariable Notes\n> +\tvariable Body\n> +\tvariable IsCompleted\n\nNitpick: Please use snake_case, here and in other places.\n\n> +\n> +\tconstructor {body} {\n> +\t\tset Notes [list]\n> +\t\tset Body $body\n> +\t\tset IsCompleted 0\n> +\t}\n> +\n> +\tmethod eval {script} {\n> +\t\tnamespace eval [self] $script\n> +\t}\n> +\n> +\tmethod add_note {} {\n> +\t\tif {$IsCompleted} { error \"Cannot add a note to a completed chord\" }\n> +\n> +\t\tset note [ChordNote new [self]]\n> +\n> +\t\tlappend Notes $note\n> +\n> +\t\treturn $note\n> +\t}\n> +\n> +\tmethod notify_note_activation {} {\n\nSince this method is for internal use only, can it be made \"private\"? \nDoes the OO library support something like this?\n\n> +\t\tif {!$IsCompleted} {\n> +\t\t\tforeach note $Notes {\n> +\t\t\t\tif {![$note is_activated]} { return }\n> +\t\t\t}\n> +\n> +\t\t\tset IsCompleted 1\n> +\n> +\t\t\tnamespace eval [self] $Body\n> +\t\t\tnamespace delete [self]\n> +\t\t}\n> +\t}\n> +}\n> +\n> +oo::class create ChordNote {\n> +\tvariable Chord IsActivated\n> +\n> +\tconstructor {chord} {\n> +\t\tset Chord $chord\n> +\t\tset IsActivated 0\n> +\t}\n> +\n> +\tmethod is_activated {} {\n> +\t\treturn $IsActivated\n> +\t}\n> +\n> +\tmethod unknown {} {\n\nI'm a bit lost here. This method is named 'unknown', but searching for \n'unknown' in this patch just gives me two results: this line here, and \nthen one in a comment at the start of the file.\n\nFrom what I understand looking at the code, it some sort of a \"default\" \nmethod, and is called when you run just `$chord_note`. How exactly is \nthis method designated to be the default?\n\nAlso, \"unknown\" makes little sense in this context. Can you rename it to \nsomething more meaningful? Maybe something like \"activate_note\"?\n\n> +\t\tif {!$IsActivated} {\n> +\t\t\tset IsActivated 1\n> +\t\t\t$Chord notify_note_activation\n> +\t\t}\n> +\t}\n> +}\n\nFrom what I understand, the \"Note\" object is effectively used as a \ncount. There is no other state associated with it. When I first heard of \nyour description of this abstraction, I assumed that a Note would also \nstore a script to execute with it. So, when you \"activate\" a note, it \nwould first execute the script, and then mark itself as \"activated\", and \nnotify the chord. Would that abstraction make more sense?\n\nI don't really mind keeping it this way, but I wonder if that design \nwould make the abstraction easier to wrap your head around.\n\n> diff --git a/lib/index.tcl b/lib/index.tcl\n> index 28d4d2a54e..64046d6833 100644\n> --- a/lib/index.tcl\n> +++ b/lib/index.tcl\n> @@ -7,7 +7,7 @@ proc _delete_indexlock {} {\n>  \t}\n>  }\n>  \n> -proc _close_updateindex {fd after} {\n> +proc _close_updateindex {fd} {\n>  \tglobal use_ttk NS\n>  \tfconfigure $fd -blocking 1\n>  \tif {[catch {close $fd} err]} {\n> @@ -52,8 +52,6 @@ proc _close_updateindex {fd after} {\n>  \t}\n>  \n>  \t$::main_status stop\n> -\tunlock_index\n> -\tuplevel #0 $after\n\nThere is a call to unlock_index in the body of the if statement above \ntoo. Do we want to remove that too, or should it be left alone?\n\nBut immediately after the unlocking of the index there, a call to \n`rescan` is made. `rescan` acquired the lock, so it would fail if we do \nnot unlock the index there. Note that `rescan` itself is asynchronous. \n\nSince every call to `_close_updateindex` is followed by an index unlock, \nit would mean the index would be unlocked for the rescan while it is in \nprogress (for all calls other than the one from `write_checkout_index`). \nWhat a mess!\n\nThat codepath seems to be taken when a major error happens, and we just \nresign to our fate and get a fresh start by doing a rescan and syncing \nthe repo state. So it is quite likely whatever operation we were doing \nfailed spectacularly.\n\nMaybe the answer is to swallow the bitter pill and introduce a \nswitch/boolean in `_close_updateindex` that controls whether the index \nis unlocked or not. We unlock it when the if statement is not taken, and \nkeep the current codepath when it is. I call it a \"bitter pill\" because \nI'm usually not a huge fan of adding knobs like that in functions. Makes \nthe function harder to reason about and makes it more bug prone.\n\nIf you can think of a better/cleaner way of working around this, \nsuggestions are welcome!\n\n>  }\n>  \n>  proc update_indexinfo {msg path_list after} {\n> @@ -90,7 +88,9 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t_close_updateindex $fd\n> +\t\tunlock_index\n> +\t\tuplevel #0 $after\n>  \t\treturn\n>  \t}\n>  \n> @@ -156,7 +156,9 @@ proc write_update_index {fd path_list total_cnt batch after} {\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t_close_updateindex $fd\n> +\t\tunlock_index\n> +\t\tuplevel #0 $after\n>  \t\treturn\n>  \t}\n>  \n> @@ -233,7 +235,8 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t_close_updateindex $fd $do_unlock_index $after\n> +\t\tuplevel #0 $after\n\n_close_updateindex takes only one argument, and you pass it 3. \n$do_unlock_index does not seem to be defined anywhere. $after is \nevaluated just after this line, and _close_updateindex doesn't accept \nthe argument anyway. I suspect this is a leftover from a different \napproach you tried before this one.\n\nAlso, unlike all the other places where _close_updateindex is used, this \none does not make a call to unlock_index. Is that intended? IIUC, it \nshould be intended, since this is the part which uses the \"chord\", but a \nconfirmation would be nice.\n\n>  \t\treturn\n>  \t}\n>  \n> @@ -393,61 +396,266 @@ proc revert_helper {txt paths} {\n>  \n>  \tif {![lock_index begin-update]} return\n>  \n> +\t# Common \"after\" functionality that waits until multiple asynchronous\n> +\t# operations are complete (by waiting for them to activate their notes\n> +\t# on the chord).\n\nNitpick: mention what the \"multiple asynchronous operations\" are exactly \n(i.e, they are the deletion and index checkout operations).\n\n> +\tset after_chord [SimpleChord new {\n> +\t\tunlock_index\n> +\t\tif {$should_reshow_diff} { reshow_diff }\n> +\t\tui_ready\n> +\t}]\n> +\n> +\t$after_chord eval { set should_reshow_diff 0 }\n> +\n> +\t# We don't know how many notes we're going to create (it's dynamic based\n> +\t# on conditional paths below), so create a common note that will delay\n> +\t# the chord's completion until we activate it, and then activate it\n> +\t# after all the other notes have been created.\n> +\tset after_common_note [$after_chord add_note]\n> +\n>  \tset path_list [list]\n> +\tset untracked_list [list]\n>  \tset after {}\n\n'after' seems to be an unused variable. This line can be deleted.\n\n>  \tforeach path $paths {\n>  \t\tswitch -glob -- [lindex $file_states($path) 0] {\n>  \t\tU? {continue}\n> +\t\t?O {\n> +\t\t\tlappend untracked_list $path\n> +\t\t}\n>  \t\t?M -\n>  \t\t?T -\n>  \t\t?D {\n>  \t\t\tlappend path_list $path\n>  \t\t\tif {$path eq $current_diff_path} {\n> -\t\t\t\tset after {reshow_diff;}\n> +\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n>  \t\t\t}\n>  \t\t}\n>  \t\t}\n>  \t}\n>  \n> +\tset path_cnt [llength $path_list]\n> +\tset untracked_cnt [llength $untracked_list]\n>  \n> -\t# Split question between singular and plural cases, because\n> -\t# such distinction is needed in some languages. Previously, the\n> -\t# code used \"Revert changes in\" for both, but that can't work\n> -\t# in languages where 'in' must be combined with word from\n> -\t# rest of string (in different way for both cases of course).\n> -\t#\n> -\t# FIXME: Unfortunately, even that isn't enough in some languages\n> -\t# as they have quite complex plural-form rules. Unfortunately,\n> -\t# msgcat doesn't seem to support that kind of string translation.\n> -\t#\n> -\tset n [llength $path_list]\n> -\tif {$n == 0} {\n> -\t\tunlock_index\n> -\t\treturn\n> -\t} elseif {$n == 1} {\n> -\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n> -\t} else {\n> -\t\tset query [mc \"Revert changes in these %i files?\" $n]\n> -\t}\n> +\tif {$path_cnt > 0} {\n> +\t\t# Split question between singular and plural cases, because\n> +\t\t# such distinction is needed in some languages. Previously, the\n> +\t\t# code used \"Revert changes in\" for both, but that can't work\n> +\t\t# in languages where 'in' must be combined with word from\n> +\t\t# rest of string (in different way for both cases of course).\n> +\t\t#\n> +\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n> +\t\t# as they have quite complex plural-form rules. Unfortunately,\n> +\t\t# msgcat doesn't seem to support that kind of string\n> +\t\t# translation.\n> +\t\t#\n> +\t\tif {$path_cnt == 1} {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Revert changes in file %s?\" \\\n> +\t\t\t\t[short_path [lindex $path_list]] \\\n> +\t\t\t\t]\n> +\t\t} else {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Revert changes in these %i files?\" \\\n> +\t\t\t\t$path_cnt]\n> +\t\t}\n>  \n> -\tset reply [tk_dialog \\\n> -\t\t.confirm_revert \\\n> -\t\t\"[appname] ([reponame])\" \\\n> -\t\t\"$query\n> +\t\tset reply [tk_dialog \\\n> +\t\t\t.confirm_revert \\\n> +\t\t\t\"[appname] ([reponame])\" \\\n> +\t\t\t\"$query\n>  \n>  [mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n> -\t\tquestion \\\n> -\t\t1 \\\n> -\t\t[mc \"Do Nothing\"] \\\n> -\t\t[mc \"Revert Changes\"] \\\n> -\t\t]\n> -\tif {$reply == 1} {\n> -\t\tcheckout_index \\\n> -\t\t\t$txt \\\n> +\t\t\tquestion \\\n> +\t\t\t1 \\\n> +\t\t\t[mc \"Do Nothing\"] \\\n> +\t\t\t[mc \"Revert Changes\"] \\\n> +\t\t\t]\n> +\n> +\t\tif {$reply == 1} {\n> +\t\t\tcheckout_index \\\n> +\t\t\t\t$txt \\\n> +\t\t\t\t$path_list \\\n> +\t\t\t\t[$after_chord add_note]\n> +\t\t}\n> +\t}\n> +\n> +\tif {$untracked_cnt > 0} {\n> +\t\t# Split question between singular and plural cases, because\n> +\t\t# such distinction is needed in some languages.\n> +\t\t#\n> +\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n> +\t\t# as they have quite complex plural-form rules. Unfortunately,\n> +\t\t# msgcat doesn't seem to support that kind of string\n> +\t\t# translation.\n> +\t\t#\n> +\t\tif {$untracked_cnt == 1} {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Delete untracked file %s?\" \\\n> +\t\t\t\t[short_path [lindex $untracked_list]] \\\n> +\t\t\t\t]\n> +\t\t} else {\n> +\t\t\tset query [mc \\\n> +\t\t\t\t\"Delete these %i untracked files?\" \\\n> +\t\t\t\t$untracked_cnt \\\n> +\t\t\t\t]\n> +\t\t}\n> +\n> +\t\tset reply [tk_dialog \\\n> +\t\t\t.confirm_revert \\\n> +\t\t\t\"[appname] ([reponame])\" \\\n> +\t\t\t\"$query\n> +\n> +[mc \"Files will be permanently deleted.\"]\" \\\n> +\t\t\tquestion \\\n> +\t\t\t1 \\\n> +\t\t\t[mc \"Do Nothing\"] \\\n> +\t\t\t[mc \"Delete Files\"] \\\n> +\t\t\t]\n> +\n> +\t\tif {$reply == 1} {\n> +\t\t\t$after_chord eval { set should_reshow_diff 1 }\n> +\n> +\t\t\tdelete_files $untracked_list [$after_chord add_note]\n> +\t\t}\n> +\t}\n> +\n> +\t# Activate the common note. If no other notes were created, this\n> +\t# completes the chord. If other notes were created, then this common\n> +\t# note prevents a race condition where the chord might complete early.\n> +\t$after_common_note\n> +}\n> +\n> +# Delete all of the specified files, performing deletion in batches to allow the\n> +# UI to remain responsive and updated.\n> +proc delete_files {path_list after} {\n> +\t# Enable progress bar status updates\n> +\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n> +\n> +\tset path_index 0\n> +\tset deletion_errors [list]\n> +\tset batch_size 50\n> +\n> +\tdelete_helper \\\n> +\t\t$path_list \\\n> +\t\t$path_index \\\n> +\t\t$deletion_errors \\\n> +\t\t$batch_size \\\n> +\t\t$after\n> +}\n> +\n> +# Helper function to delete a list of files in batches. Each call deletes one\n> +# batch of files, and then schedules a call for the next batch after any UI\n> +# messages have been processed.\n> +proc delete_helper {path_list path_index deletion_errors batch_size after} {\n> +\tglobal file_states\n> +\n> +\tset path_cnt [llength $path_list]\n> +\n> +\tset batch_remaining $batch_size\n> +\n> +\twhile {$batch_remaining > 0} {\n> +\t\tif {$path_index >= $path_cnt} { break }\n> +\n> +\t\tset path [lindex $path_list $path_index]\n> +\n> +\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n> +\n> +\t\tif {$deletion_failed} {\n> +\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n> +\t\t} else {\n> +\t\t\tremove_empty_directories [file dirname $path]\n> +\n> +\t\t\t# Don't assume the deletion worked. Remove the file from\n> +\t\t\t# the UI, but only if it no longer exists.\n> +\t\t\tif {![path_exists $path]} {\n> +\t\t\t\tunset file_states($path)\n> +\t\t\t\tdisplay_file $path __\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\tincr path_index 1\n> +\t\tincr batch_remaining -1\n> +\t}\n> +\n> +\t# Update the progress bar to indicate that this batch has been\n> +\t# completed. The update will be visible when this procedure returns\n> +\t# and allows the UI thread to process messages.\n> +\t$::main_status update $path_index $path_cnt\n> +\n> +\tif {$path_index < $path_cnt} {\n> +\t\t# The Tcler's Wiki lists this as the best practice for keeping\n> +\t\t# a UI active and processing messages during a long-running\n> +\t\t# operation.\n> +\n> +\t\tafter idle [list after 0 [list \\\n> +\t\t\tdelete_helper \\\n>  \t\t\t$path_list \\\n> -\t\t\t[concat $after [list ui_ready]]\n> +\t\t\t$path_index \\\n> +\t\t\t$deletion_errors \\\n> +\t\t\t$batch_size \\\n> +\t\t\t$after\n> +\t\t\t]]\n>  \t} else {\n> -\t\tunlock_index\n> +\t\t# Finish the status bar operation.\n> +\t\t$::main_status stop\n> +\n> +\t\t# Report error, if any, based on how many deletions failed.\n> +\t\tset deletion_error_cnt [llength $deletion_errors]\n> +\n> +\t\tif {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n\nNitpick: please split the line into two.\n\n> +\t\t\tset error_text \"Encountered errors deleting files:\\n\"\n\nWrap the string in a `mc [...]` so it can be translated some time in the \nfuture.\n\n> +\n> +\t\t\tforeach deletion_error $deletion_errors {\n> +\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n> +\t\t\t}\n> +\n> +\t\t\terror_popup $error_text\n> +\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n> +\t\t\terror_popup [mc \\\n> +\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n> +\t\t\t\t$path_cnt \\\n> +\t\t\t\t]\n> +\t\t} elseif {$deletion_error_cnt > 1} {\n> +\t\t\terror_popup [mc \\\n> +\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n> +\t\t\t\t$deletion_error_cnt \\\n> +\t\t\t\t$path_cnt \\\n> +\t\t\t\t]\n\nNice! In case someone in the future wants to have a config variable to \nchange this limit, this makes it pretty easy to do so. \n\n> +\t\t}\n> +\n> +\t\tuplevel #0 $after\n> +\t}\n> +}\n> +\n> +proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n\nWhy use a procedure, and not a global variable? My guess is to make it \nimpossible for some code to change this value by mistake. Do I guess \ncorrectly?\n\n> +\n> +# This function is from the TCL documentation:\n> +#\n> +#   https://wiki.tcl-lang.org/page/file+exists\n> +#\n> +# [file exists] returns false if the path does exist but is a symlink to a path\n> +# that doesn't exist. This proc returns true if the path exists, regardless of\n> +# whether it is a symlink and whether it is broken.\n> +proc path_exists {name} {\n> +\texpr {![catch {file lstat $name finfo}]}\n> +}\n> +\n> +# Remove as many empty directories as we can starting at the specified path,\n> +# walking up the directory tree. If we encounter a directory that is not\n> +# empty, or if a directory deletion fails, then we stop the operation and\n> +# return to the caller. Even if this procedure fails to delete any\n> +# directories at all, it does not report failure.\n> +proc remove_empty_directories {directory_path} {\n> +\tset parent_path [file dirname $directory_path]\n> +\n> +\twhile {$parent_path != $directory_path} {\n> +\t\tset contents [glob -nocomplain -dir $directory_path *]\n> +\n> +\t\tif {[llength $contents] > 0} { break }\n> +\t\tif {[catch {file delete -- $directory_path}]} { break }\n> +\n> +\t\tset directory_path $parent_path\n> +\t\tset parent_path [file dirname $directory_path]\n>  \t}\n>  }\n\nWew! This took longer than I expected ;)\n\nTested on Linux. Works fine after fixing the extra arguments passed to \n`_close_updateindex`. Thanks.\n\n[0] https://www.tcl.tk/man/tcl8.6/TclCmd/class.htm\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385923","messageId":"20191111193546.lsx24e3w6fo336hu@yadavpratyush.com","threadId":"52154","inReplyTo":"pull.436.v2.git.1573110335.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-11T19:35:46Z","receivedAt":"2019-11-11T19:35:52Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"On 07/11/19 07:05AM, Jonathan Gilbert via GitGitGadget wrote:\n> My development environment sometimes makes automatic changes that I don't\n> want to keep. In some cases, this involves new files being added that I\n> don't want to commit or keep (but I also don't want to outright .gitignore \n> forever). I have typically had to explicitly delete those files externally\n> to Git Gui, which is a context switch to a manual operation, and I want to\n> be able to just select those newly-created untracked files in the UI and\n> \"revert\" them into oblivion.\n> \n> This change updates the revert_helper proc to check for untracked files as\n> well as changes, and then changes to be reverted and untracked files are\n> handled by independent blocks of code. The user is prompted independently\n> for untracked files, since the underlying action is fundamentally different\n> (rm -f). If after deleting untracked files, the directory containing them\n> becomes empty, then the directory is removed as well. A new proc \n> delete_files takes care of actually deleting the files, using the Tcler's\n> Wiki recommended approach for keeping the UI responsive.\n> \n> Since the checkout_index and delete_files calls are both asynchronous and\n> could potentially complete in any order, a \"chord\" is used to coordinate\n> unlocking the index and returning the UI to a usable state only after both\n> operations are complete.\n> \n> This is the third revision of this change, which differs from the second\n> version in the following ways:\n> \n>  * A new construct called a \"chord\" is used to coordinate the completion of\n>    multiple asynchronous operations that can be kicked off by revert_helper.\n>    A chord is, conceptually, a procedure with multiple entrypoints whose\n>    body only executes once all entrypoints have been activated. The \n>    chord.tcl file includes comprehensive documentation of how to use the\n>    chord classes.\n>    \n>    \n>  * Since we might not yet be ready to unlock the index when checkout_index \n>    returns, the _close_updateindex proc where it was ultimately unlocking\n>    the index has been modified so that unlocking the index is the\n>    responsibility of the caller. Since the $after functionality ran after \n>    unlock_index, that is also hoisted out. Nothing in _close_updateindex \n>    appears to be asynchronous, so the caller can simply make the calls\n>    itself upon its return.\n\nThe cover letter is so much more descriptive than the commit message. It \nwould be nice to have all this context and commentary in the commit \nmessage. Of course, you'd remove the \"personal workflow\" bit and some \nother stuff, but most of this can be copied verbatim.\n\nAlso, like I mentioned in the review of your second patch, \n`_close_updateindex` _does_ have an asynchronous component \nunfortunately.\n    \n>    \n>  * lexists has been renamed to path_exists.\n>    \n>    \n>  * Up to 10 deletion errors are now shown simultaneously. I also confirmed\n>    that Tcl's file delete code will always return a nicely-formatted error\n>    including the filename, and changed the message so that it isn't also \n>    injecting the filename.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"385951","messageId":"CAPSOpYv0NNihiWF2LL=czoP7N2CGNSkB8SBNTZ65iOZVOBPqbA@mail.gmail.com","threadId":"52154","inReplyTo":"20191111192526.crllpe3phitneu3p@yadavpratyush.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-11T21:55:13Z","receivedAt":"2019-11-11T21:55:29Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Mon, Nov 11, 2019 at 1:25 PM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> On 07/11/19 07:05AM, Jonathan Gilbert via GitGitGadget wrote:\n> > --- /dev/null\n> > +++ b/lib/chord.tcl\n> > @@ -0,0 +1,137 @@\n>\n> The 'class' documentation [0] suggests adding a \"package require TclOO\".\n> But TclOO ships by default with Tcl 8.6 and above. So, I'm not really\n> sure if we need this.\n\nI'm not super familiar with it. I just checked what Tcl version I was\nmyself running, since it's only there because of the Git Gui\ninstallation bundled with Git for Windows, and it was 8.6, so I\nassumed it was fair game to use. It didn't occur to me that you could\nalready have an older version of Tcl installed and have Git Gui use\nit. :-) So, if I'm understanding correctly, `TclOO` as a package could\npotentially be used to allow TclOO to be used with 8.4, the minimum\nsupported version you mention below, and it just happened to work for\nme in my testing without that because I have 8.6 installed but that's\ntechnically newer than the supported baseline?\n\n> Nice to see some good documentation!\n>\n> One nitpick: would it make more sense to have the documentation for a\n> method/constructor just above that method/constructor? This way, when\n> someone updates the code some time later, they'll also hopefully\n> remember to update the documentation. It is much more likely to be stale\n> if all of it just stays on the top.\n\nHmm, what do you think of both? I was thinking of the documentation as\na single self-contained block that someone could read to put together\nan understanding of how the chord system fits together, and split out,\nit wouldn't have that readability. What about a more abstract\ndescription in a block at the top, and then more technically-detailed\n& specific descriptions attached to each method?\n\n> > +oo::class create SimpleChord {\n>\n> This comes from the TclOO package, right?\n>\n> git-gui has its own object-oriented system (lib/class.tcl). It was\n> written circa 2007. I suspect something like TclOO did not exist back\n> then.\n>\n> Why not use that? Does it have some limitations that TclOO does not\n> have? I do not mind using the \"official\" OO system. I just want to know\n> why exactly you made the choice.\n\nHaving limited experience with Tcl, I did a Google search for \"tcl\nobject oriented\" and ended up writing code using TclOO because that's\nwhat came up. Do you think it makes sense to rework this to use\n`class.tcl`, or perhaps instead the opposite: have a policy of using\nthe standard TclOO going forward, and let the rest of Git Gui\norganically upgrade itself to some hypothetical point in the future\nwhere class.tcl is no longer used by anything?\n\n> More importantly, TclOO ships as part of the core distribution with Tcl\n> 8.6, but as of now the minimum version required for git-gui is 8.4. So,\n> I think we should bump the minimum version (8.6 released circa 2012, so\n> most people should have caught up by now I hope).\n\nIf I understand correctly, you mentioned that TclOO was intrinsically\navailable to me because I was using Tcl 8.6, and that the manual\nrecommends `package require TclOO` -- does that package dependency\npermit the use of TclOO on 8.4? If so, could that be a way to avoid\nbumping the minimum version required? Simply in the interest of\nkeeping the scope of the change limited. If not, then bumping the\nminimum required version to 8.6 from 2012 doesn't seem entirely\nunreasonable either. :-)\n\n> > +     variable Notes\n> > +     variable Body\n> > +     variable IsCompleted\n>\n> Nitpick: Please use snake_case, here and in other places.\n\nOkay, yep -- I had copied the convention that I saw in TclOO examples,\nconscious of the fact that there might be a standard specific to\nobject-oriented Tcl.\n\n> > +     method notify_note_activation {} {\n>\n> Since this method is for internal use only, can it be made \"private\"?\n> Does the OO library support something like this?\n\nI don't think so, because it's called from outside the class. What\nwe'd be looking for is something like C++'s \"friend\" syntax. Tcl\ndoesn't seem to have this. Though, I just did some further Googling,\nand saw a hint that it might be possible to bypass member security on\na case-by-case basis, so that the method is private but `ChordNote` is\nable to call it anyway. I'll see if I can't figure this out. :-)\n\n> > +     method unknown {} {\n>\n> I'm a bit lost here. This method is named 'unknown', but searching for\n> 'unknown' in this patch just gives me two results: this line here, and\n> then one in a comment at the start of the file.\n>\n> From what I understand looking at the code, it some sort of a \"default\"\n> method, and is called when you run just `$chord_note`. How exactly is\n> this method designated to be the default?\n>\n> Also, \"unknown\" makes little sense in this context. Can you rename it to\n> something more meaningful? Maybe something like \"activate_note\"?\n\nI think it's the fact that it is named `unknown` that makes it the\n\"default\" method. I think this just needs documentary comments next to\nit. The TclOO documentation says:\n\n> obj unknown ?methodName? ?arg ...?\n> This method is called when an attempt to invoke the method methodName on\n> object obj fails. The arguments that the user supplied to the method are\n> given as arg arguments. If methodName is absent, the object was invoked with\n> no method name at all (or any other arguments).\n\nIt was based on that last sentence that I interpreted `unknown` as,\n\"This is a mechanism for making an object that can be called like a\nmethod.\"\n\n> > +             if {!$IsActivated} {\n> > +                     set IsActivated 1\n> > +                     $Chord notify_note_activation\n> > +             }\n> > +     }\n> > +}\n>\n> From what I understand, the \"Note\" object is effectively used as a\n> count. There is no other state associated with it. When I first heard of\n> your description of this abstraction, I assumed that a Note would also\n> store a script to execute with it. So, when you \"activate\" a note, it\n> would first execute the script, and then mark itself as \"activated\", and\n> notify the chord. Would that abstraction make more sense?\n>\n> I don't really mind keeping it this way, but I wonder if that design\n> would make the abstraction easier to wrap your head around.\n\nI learned about the concept of chords and notes from an experimental\nlanguage that Microsoft created many years back called \"Polyphonic C#\"\n(which in turn got rolled into \"Cw\" (C-omega)), and in that\nabstraction, the idea was that, well, as a baseline, for starters, we\nhave methods and each one, conceptually, has an entrypoint with a\ncertain set of parameters, and when you call that entrypoint, the\nparameters are all set and the body runs. With a \"chord\", you have\nmore than one entrypoint attached to the same body -- the entrypoints\nthemselves don't have any logic associated with them individually.\nEach note has its own parameter list, and when all the notes have been\ncalled, the body is run with _all_ of those parameters.\n\nI drew some ASCII art, don't know if it'll translate in the message,\nbut here goes :-)\n\nBasic method (or, if you will, a \"chord\" with only one \"note\"):\n\n           (caller)\n              |\n    void Add(int X, int Y)\n              |\n      { output(X + Y) }\n\nA \"chord\" with two \"notes\":\n\n        (caller)                (caller)\n            |                       |\n    void AddX(int X)         void AddY(int Y)\n            |                       |\n            `-----------.-----------'\n                        |\n                { output(X + Y) }\n\nThe specific details differ from what I've written here. In Polyphonic\nC#, you don't have to instantiate a chord, you simply start calling\nmethods, and the runtime matches up complete sets dynamically. (Just\nthinking through the implications of this, if the notes aren't all\ncalled at exactly the same rate this obviously leads very easily to\nbugs that chew up all memory on incomplete chords. :-P) Also,\nMicrosoft's language has parameters to each of the notes that are\n_all_ passed to the body at once. My implementation here is a \"simple\"\nchord, I didn't bother with arguments, as they aren't needed in this\nusage :-) I also found it much simpler to think of implementing the\nchord with the activations being explicit instead of implicit. So\ninstead of saying up front, \"Here is my method body and here are its 3\nentrypoints\", with this implementation the chord is a dynamic object,\nyou say \"Here is my method body\" and get back a thing that you can\nstart tacking entrypoints onto.\n\nBut, a \"note\" in a SimpleChord isn't a counter, it's a latch. The\nchord itself is acting sort of like a counter, in that all the notes\nneed to be activated, but because the notes are latches, activating a\nnote repeatedly has the same effect as activating it once. There's no\nway for one note to interfere with other notes, which wouldn't be the\ncase if it literally were just a counter.\n\nIt seems to me that a chord where each note has a script of its own is\nactually basically just a class with methods, I guess with a common\njoined epilogue?:\n\n        (caller)                (caller)\n            |                       |\n    void AddX(int X)         void AddY(int Y)\n            |                       |\n   { script for AddX }      {script for AddY }\n            |                       |\n            `-----------.-----------'\n                        |\n                { common tail?? }\n\nThe whole point is that the notes are conceptually different \"headers\"\ninto _the same_ body. When you call a note of a chord, it is because\nyou want the _chord_'s script to run, and the chord is acting as a\nconstruct that says \"okay, yes, I'll satisfy your request that I\nexecute, but you'll have to wait, because I'm going to satisfy _all_\nyour requests in one go\".\n\n> >       $::main_status stop\n> > -     unlock_index\n> > -     uplevel #0 $after\n>\n> There is a call to unlock_index in the body of the if statement above\n> too. Do we want to remove that too, or should it be left alone?\n>\n> That codepath seems to be taken when a major error happens, and we just\n> resign to our fate and get a fresh start by doing a rescan and syncing\n> the repo state. So it is quite likely whatever operation we were doing\n> failed spectacularly.\n>\n> Maybe the answer is to swallow the bitter pill and introduce a\n> switch/boolean in `_close_updateindex` that controls whether the index\n> is unlocked or not. We unlock it when the if statement is not taken, and\n> keep the current codepath when it is. I call it a \"bitter pill\" because\n> I'm usually not a huge fan of adding knobs like that in functions. Makes\n> the function harder to reason about and makes it more bug prone.\n>\n> If you can think of a better/cleaner way of working around this,\n> suggestions are welcome!\n\nHmm, so, yeah, the entire if statement only occurs if it can't close\nthe file descriptor. Is that something that actually happens? If so,\nthen it should perhaps be throwing an exception, because having\nstarted a rescan is probably more than the caller bargained for. That\nwould prevent the callers from unlocking the index out from under the\nrescan, and also cancel any other processing they might be doing that\nis probably making bad assumptions with a rescan running.\n\n> >       if {$update_index_cp >= $total_cnt} {\n> > -             _close_updateindex $fd $after\n> > +             _close_updateindex $fd $do_unlock_index $after\n>\n> _close_updateindex takes only one argument, and you pass it 3.\n> $do_unlock_index does not seem to be defined anywhere. $after is\n> evaluated just after this line, and _close_updateindex doesn't accept\n> the argument anyway. I suspect this is a leftover from a different\n> approach you tried before this one.\n\nIt is indeed, oops!\n\n> Also, unlike all the other places where _close_updateindex is used, this\n> one does not make a call to unlock_index. Is that intended? IIUC, it\n> should be intended, since this is the part which uses the \"chord\", but a\n> confirmation would be nice.\n\nIntentional, yes. I'll see if there's a concise way to document this.\n\n> > +     # Common \"after\" functionality that waits until multiple asynchronous\n> > +     # operations are complete (by waiting for them to activate their notes\n> > +     # on the chord).\n>\n> Nitpick: mention what the \"multiple asynchronous operations\" are exactly\n> (i.e, they are the deletion and index checkout operations).\n\nOkeydoke.\n\n> >       set after {}\n>\n> 'after' seems to be an unused variable. This line can be deleted.\n\nGood catch.\n\n> > +             if {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n>\n> Nitpick: please split the line into two.\n\nWill do.\n\n> > +                     set error_text \"Encountered errors deleting files:\\n\"\n>\n> Wrap the string in a `mc [...]` so it can be translated some time in the\n> future.\n\nAh, yes, I did that with most messages, this was an oversight.\n\n> > +proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n>\n> Why use a procedure, and not a global variable? My guess is to make it\n> impossible for some code to change this value by mistake. Do I guess\n> correctly?\n\nA variable is by definition not a constant. This is the pattern that\ncame up when I did a search for how one makes a constant in Tcl. \"\"\\_(\n``_/ )_/\"\"\n\nMaking it a procedure means that if someone wants to put actual logic\nbehind it in the future, it's already being called as a proc.\n\n> Wew! This took longer than I expected ;)\n>\n> Tested on Linux. Works fine after fixing the extra arguments passed to\n> `_close_updateindex`. Thanks.\n\nYeah, I did run things as I was changing them to verify, and felt like\nI covered everything, I'm surprised I didn't bump into that, obviously\nI didn't cover everything after all. Perfect demonstration of why\ndevelopers should never be exclusively responsible for testing their\nown code :-D\n\nLet me know w.r.t. which OO framework to employ and what that means\nfor minimum required versions and/or package references.\n\nThanks very much,\n\nJonathan Gilbert\n"},{"id":"385952","messageId":"a1dd45d5-8dac-e86f-c964-957ff0b8dfb8@iee.email","threadId":"52154","inReplyTo":"CAPSOpYv0NNihiWF2LL=czoP7N2CGNSkB8SBNTZ65iOZVOBPqbA@mail.gmail.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2019-11-11T22:59:51Z","receivedAt":"2019-11-11T22:59:54Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 11/11/2019 21:55, Jonathan Gilbert wrote:\n> Basic method (or, if you will, a \"chord\" with only one \"note\"):\n>\n>             (caller)\n>                |\n>      void Add(int X, int Y)\n>                |\n>        { output(X + Y) }\n>\n> A \"chord\" with two \"notes\":\n>\n>          (caller)                (caller)\n>              |                       |\n>      void AddX(int X)         void AddY(int Y)\n>              |                       |\n>              `-----------.-----------'\n>                          |\n>                  { output(X + Y) }\n>\n> The specific details differ from what I've written here. In Polyphonic\n> C#, you don't have to instantiate a chord, you simply start calling\n> methods, and the runtime matches up complete sets dynamically.\nsounds like \"Currying\" a function but with the parameters taken in any \norder, though, in a sense, perhaps not generating intermediate functions...\n\nPhilip\n"},{"id":"385979","messageId":"CAPSOpYsbBX3d_oWJHtKHD7b0cXcELF=t-zYNEVpU3sf8qzD4Ew@mail.gmail.com","threadId":"52154","inReplyTo":"a1dd45d5-8dac-e86f-c964-957ff0b8dfb8@iee.email","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-12T04:49:11Z","receivedAt":"2019-11-12T04:49:30Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Mon, Nov 11, 2019 at 4:59 PM Philip Oakley <philipoakley@iee.email> wrote:\n> sounds like \"Currying\" a function but with the parameters taken in any\n> order, though, in a sense, perhaps not generating intermediate functions...\n\nIt's like currying if you could pass g(x) = f(x, y) to one block of\ncode and h(y) = f(x, y) to another block of code, so that each of g\nand h are each like curried versions of f that \"bake in\" one of the\narguments, without having to know which one will get called first. :-)\n\nJonathan Gilbert\n"},{"id":"386002","messageId":"11812626-07f7-5610-cd13-ce4bdda4f674@iee.email","threadId":"52154","inReplyTo":"CAPSOpYsbBX3d_oWJHtKHD7b0cXcELF=t-zYNEVpU3sf8qzD4Ew@mail.gmail.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2019-11-12T10:45:55Z","receivedAt":"2019-11-12T10:45:58Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 12/11/2019 04:49, Jonathan Gilbert wrote:\n> On Mon, Nov 11, 2019 at 4:59 PM Philip Oakley <philipoakley@iee.email> wrote:\n>> sounds like \"Currying\" a function but with the parameters taken in any\n>> order, though, in a sense, perhaps not generating intermediate functions...\n> It's like currying if you could pass g(x) = f(x, y) to one block of\n> code and h(y) = f(x, y) to another block of code, so that each of g\n> and h are each like curried versions of f that \"bake in\" one of the\n> arguments, without having to know which one will get called first. :-)\n>\n> Jonathan Gilbert\nSo that would be called \"Chording\"...\n(Is there a 'proper' technical term for that approach?)\nP.\n"},{"id":"386021","messageId":"CAPSOpYt71DGzkQxQDaMm88hR5bO8npKEY7tBPTypZ+aE1yp6gA@mail.gmail.com","threadId":"52154","inReplyTo":"11812626-07f7-5610-cd13-ce4bdda4f674@iee.email","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-12T16:29:36Z","receivedAt":"2019-11-12T16:30:00Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Tue, Nov 12, 2019 at 4:45 AM Philip Oakley <philipoakley@iee.email> wrote:\n> On 12/11/2019 04:49, Jonathan Gilbert wrote:\n> > On Mon, Nov 11, 2019 at 4:59 PM Philip Oakley <philipoakley@iee.email> wrote:\n> >> sounds like \"Currying\" a function but with the parameters taken in any\n> >> order, though, in a sense, perhaps not generating intermediate functions...\n> > It's like currying if you could pass g(x) = f(x, y) to one block of\n> > code and h(y) = f(x, y) to another block of code, so that each of g\n> > and h are each like curried versions of f that \"bake in\" one of the\n> > arguments, without having to know which one will get called first. :-)\n> >\n> > Jonathan Gilbert\n> So that would be called \"Chording\"...\n> (Is there a 'proper' technical term for that approach?)\n\nNot an entirely implausible term :-) The only other implementation\nI've ever seen was Microsoft's \"Polyphonic C#\", which got rolled into\nC-omega. I'm pretty sure, though, that it was never referred to as\nsomething you _do to_ a function, but rather as a _different type_ of\nfunction -- as in, the function hasn't been \"chorded\", it \"is a\nchord\". Very little literature one way or the other though, and this\nis the first actual, live use case for the structure I've encountered\nin my years of programming :-)\n\nJonathan Gilbert\n"},{"id":"386040","messageId":"20191112193528.rqjm3j42how4fae2@yadavpratyush.com","threadId":"52154","inReplyTo":"CAPSOpYv0NNihiWF2LL=czoP7N2CGNSkB8SBNTZ65iOZVOBPqbA@mail.gmail.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-12T19:35:28Z","receivedAt":"2019-11-12T19:35:39Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nOn 11/11/19 03:55PM, Jonathan Gilbert wrote:\n> On Mon, Nov 11, 2019 at 1:25 PM Pratyush Yadav me-at-yadavpratyush.com\n> |GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> > On 07/11/19 07:05AM, Jonathan Gilbert via GitGitGadget wrote:\n> > > --- /dev/null\n> > > +++ b/lib/chord.tcl\n> > > @@ -0,0 +1,137 @@\n> >\n> > The 'class' documentation [0] suggests adding a \"package require TclOO\".\n> > But TclOO ships by default with Tcl 8.6 and above. So, I'm not really\n> > sure if we need this.\n> \n> I'm not super familiar with it. I just checked what Tcl version I was\n> myself running, since it's only there because of the Git Gui\n> installation bundled with Git for Windows, and it was 8.6, so I\n> assumed it was fair game to use. It didn't occur to me that you could\n> already have an older version of Tcl installed and have Git Gui use\n> it. :-) So, if I'm understanding correctly, `TclOO` as a package could\n> potentially be used to allow TclOO to be used with 8.4, the minimum\n> supported version you mention below, and it just happened to work for\n> me in my testing without that because I have 8.6 installed but that's\n> technically newer than the supported baseline?\n> \n> > Nice to see some good documentation!\n> >\n> > One nitpick: would it make more sense to have the documentation for a\n> > method/constructor just above that method/constructor? This way, when\n> > someone updates the code some time later, they'll also hopefully\n> > remember to update the documentation. It is much more likely to be stale\n> > if all of it just stays on the top.\n> \n> Hmm, what do you think of both? I was thinking of the documentation as\n> a single self-contained block that someone could read to put together\n> an understanding of how the chord system fits together, and split out,\n> it wouldn't have that readability. What about a more abstract\n> description in a block at the top, and then more technically-detailed\n> & specific descriptions attached to each method?\n\nSince you put it this way, it does make sense to create some flow. I'm \nnot sure if these relatively simple methods warrant specific detailed \ndocumentation.\n\nSo, if you can figure out a reasonable split, that'd be great. \nOtherwise, I guess we can just stick with this.\n \n> > > +oo::class create SimpleChord {\n> >\n> > This comes from the TclOO package, right?\n> >\n> > git-gui has its own object-oriented system (lib/class.tcl). It was\n> > written circa 2007. I suspect something like TclOO did not exist back\n> > then.\n> >\n> > Why not use that? Does it have some limitations that TclOO does not\n> > have? I do not mind using the \"official\" OO system. I just want to know\n> > why exactly you made the choice.\n> \n> Having limited experience with Tcl, I did a Google search for \"tcl\n> object oriented\" and ended up writing code using TclOO because that's\n> what came up. Do you think it makes sense to rework this to use\n> `class.tcl`, or perhaps instead the opposite: have a policy of using\n> the standard TclOO going forward, and let the rest of Git Gui\n> organically upgrade itself to some hypothetical point in the future\n> where class.tcl is no longer used by anything?\n\nReplacing class.tcl would be a big effort, and seeing how things stand \nas of now in terms of active contributors, I don't think it would happen \nin the near future.\n\nSo the question really boils down to \"do we want to mix these two \nflavors of OO frameworks?\".\n\nIf TclOO gives us some benefit over our homegrown framework, or if our \nframework is in some way hard to use, then I would certainly side on \njust sticking with TclOO.\n\nIf not, it becomes a question of taste more of less. Which \nimplementation do we like more, and which more people would be \ncomfortable working with. And whether mixing the two is a good idea or \nnot.\n\nThat being said, I am more inclined towards using our homegrown \nframework just for the sake of uniformity if nothing else.\n\nSo in the end I guess the answer is I dunno.\n \n> > More importantly, TclOO ships as part of the core distribution with Tcl\n> > 8.6, but as of now the minimum version required for git-gui is 8.4. So,\n> > I think we should bump the minimum version (8.6 released circa 2012, so\n> > most people should have caught up by now I hope).\n> \n> If I understand correctly, you mentioned that TclOO was intrinsically\n> available to me because I was using Tcl 8.6, and that the manual\n> recommends `package require TclOO` -- does that package dependency\n> permit the use of TclOO on 8.4? If so, could that be a way to avoid\n> bumping the minimum version required? Simply in the interest of\n> keeping the scope of the change limited. If not, then bumping the\n> minimum required version to 8.6 from 2012 doesn't seem entirely\n> unreasonable either. :-)\n\nI looked around a bit, and it seems that TclOO would not work with 8.4 \n[0]. So, a version bump is needed. Unless, of course, you decide to use \nthe OO framework provided by class.tcl.\n\nThe version can be bumped by editing the line git-gui.sh:33.\n \n> > > +     variable Notes\n> > > +     variable Body\n> > > +     variable IsCompleted\n> >\n> > Nitpick: Please use snake_case, here and in other places.\n> \n> Okay, yep -- I had copied the convention that I saw in TclOO examples,\n> conscious of the fact that there might be a standard specific to\n> object-oriented Tcl.\n> \n> > > +     method notify_note_activation {} {\n> >\n> > Since this method is for internal use only, can it be made \"private\"?\n> > Does the OO library support something like this?\n> \n> I don't think so, because it's called from outside the class. What\n> we'd be looking for is something like C++'s \"friend\" syntax. Tcl\n> doesn't seem to have this. Though, I just did some further Googling,\n> and saw a hint that it might be possible to bypass member security on\n> a case-by-case basis, so that the method is private but `ChordNote` is\n> able to call it anyway. I'll see if I can't figure this out. :-)\n\nI don't think too much complexity/hacking is warranted for something \nlike this. If you can figure out a really simple way to do it, great! \nOtherwise, just keep it like it is.\n \n> > > +     method unknown {} {\n> >\n> > I'm a bit lost here. This method is named 'unknown', but searching for\n> > 'unknown' in this patch just gives me two results: this line here, and\n> > then one in a comment at the start of the file.\n> >\n> > From what I understand looking at the code, it some sort of a \"default\"\n> > method, and is called when you run just `$chord_note`. How exactly is\n> > this method designated to be the default?\n> >\n> > Also, \"unknown\" makes little sense in this context. Can you rename it to\n> > something more meaningful? Maybe something like \"activate_note\"?\n> \n> I think it's the fact that it is named `unknown` that makes it the\n> \"default\" method. I think this just needs documentary comments next to\n> it. The TclOO documentation says:\n\nYes, a comment explaining it is the default would be nice.\n \n> > obj unknown ?methodName? ?arg ...?\n> > This method is called when an attempt to invoke the method methodName on\n> > object obj fails. The arguments that the user supplied to the method are\n> > given as arg arguments. If methodName is absent, the object was invoked with\n> > no method name at all (or any other arguments).\n> \n> It was based on that last sentence that I interpreted `unknown` as,\n> \"This is a mechanism for making an object that can be called like a\n> method.\"\n\nLooks like this method would also be called if someone misspelled a \nmethod name for this object. So say if someone by mistake writes \n\n  $note is_activate\n\nthis method would be called. This is a clear bug. So, add a check here \nto make sure 'methodName' is actually absent. And if it isn't, display \nan error. Displaying an error to the user on a programmer error can get \nannoying. But since we don't have something like assertions in git-gui \nyet, maybe that's the best way to get bugs noticed.\n \n> > > +             if {!$IsActivated} {\n> > > +                     set IsActivated 1\n> > > +                     $Chord notify_note_activation\n> > > +             }\n> > > +     }\n> > > +}\n> >\n> > From what I understand, the \"Note\" object is effectively used as a\n> > count. There is no other state associated with it. When I first heard of\n> > your description of this abstraction, I assumed that a Note would also\n> > store a script to execute with it. So, when you \"activate\" a note, it\n> > would first execute the script, and then mark itself as \"activated\", and\n> > notify the chord. Would that abstraction make more sense?\n> >\n> > I don't really mind keeping it this way, but I wonder if that design\n> > would make the abstraction easier to wrap your head around.\n> \n> I learned about the concept of chords and notes from an experimental\n> language that Microsoft created many years back called \"Polyphonic C#\"\n> (which in turn got rolled into \"Cw\" (C-omega)), and in that\n> abstraction, the idea was that, well, as a baseline, for starters, we\n> have methods and each one, conceptually, has an entrypoint with a\n> certain set of parameters, and when you call that entrypoint, the\n> parameters are all set and the body runs. With a \"chord\", you have\n> more than one entrypoint attached to the same body -- the entrypoints\n> themselves don't have any logic associated with them individually.\n> Each note has its own parameter list, and when all the notes have been\n> called, the body is run with _all_ of those parameters.\n> \n> I drew some ASCII art, don't know if it'll translate in the message,\n> but here goes :-)\n> \n> Basic method (or, if you will, a \"chord\" with only one \"note\"):\n> \n>            (caller)\n>               |\n>     void Add(int X, int Y)\n>               |\n>       { output(X + Y) }\n> \n> A \"chord\" with two \"notes\":\n> \n>         (caller)                (caller)\n>             |                       |\n>     void AddX(int X)         void AddY(int Y)\n>             |                       |\n>             `-----------.-----------'\n>                         |\n>                 { output(X + Y) }\n> \n> The specific details differ from what I've written here. In Polyphonic\n> C#, you don't have to instantiate a chord, you simply start calling\n> methods, and the runtime matches up complete sets dynamically. (Just\n> thinking through the implications of this, if the notes aren't all\n> called at exactly the same rate this obviously leads very easily to\n> bugs that chew up all memory on incomplete chords. :-P) Also,\n> Microsoft's language has parameters to each of the notes that are\n> _all_ passed to the body at once. My implementation here is a \"simple\"\n> chord, I didn't bother with arguments, as they aren't needed in this\n> usage :-) I also found it much simpler to think of implementing the\n> chord with the activations being explicit instead of implicit. So\n> instead of saying up front, \"Here is my method body and here are its 3\n> entrypoints\", with this implementation the chord is a dynamic object,\n> you say \"Here is my method body\" and get back a thing that you can\n> start tacking entrypoints onto.\n> \n> But, a \"note\" in a SimpleChord isn't a counter, it's a latch. The\n> chord itself is acting sort of like a counter, in that all the notes\n> need to be activated, but because the notes are latches, activating a\n> note repeatedly has the same effect as activating it once. There's no\n> way for one note to interfere with other notes, which wouldn't be the\n> case if it literally were just a counter.\n\nMakes sense.\n \n> It seems to me that a chord where each note has a script of its own is\n> actually basically just a class with methods, I guess with a common\n> joined epilogue?:\n> \n>         (caller)                (caller)\n>             |                       |\n>     void AddX(int X)         void AddY(int Y)\n>             |                       |\n>    { script for AddX }      {script for AddY }\n>             |                       |\n>             `-----------.-----------'\n>                         |\n>                 { common tail?? }\n\nThanks for explaining.\n\nI had a slightly different mental model of the abstraction. The figure \nhere is what I had in mind, with the exception being that the two \nfunctions that the two callers call are independent of each other.\n\nTo put it in more detail, what I was thinking of was that you'd create a \nbunch of scripts that had to be evaluated separately, independent of \neach other. Each script is associated with a note. Activating a note \nruns that script. And when all the notes are activated, the common tail \nis executed.\n\nAs far as I see, the use of the chord in the patch has just two \nindependent operations that need to run a common tail once both are \ncomplete.\n\nThat's not to say it has to be done this way. Your way works just as \nwell, just in a slightly different way :)\n \n> The whole point is that the notes are conceptually different \"headers\"\n> into _the same_ body. When you call a note of a chord, it is because\n> you want the _chord_'s script to run, and the chord is acting as a\n> construct that says \"okay, yes, I'll satisfy your request that I\n> execute, but you'll have to wait, because I'm going to satisfy _all_\n> your requests in one go\".\n> \n> > >       $::main_status stop\n> > > -     unlock_index\n> > > -     uplevel #0 $after\n> >\n> > There is a call to unlock_index in the body of the if statement above\n> > too. Do we want to remove that too, or should it be left alone?\n> >\n> > That codepath seems to be taken when a major error happens, and we just\n> > resign to our fate and get a fresh start by doing a rescan and syncing\n> > the repo state. So it is quite likely whatever operation we were doing\n> > failed spectacularly.\n> >\n> > Maybe the answer is to swallow the bitter pill and introduce a\n> > switch/boolean in `_close_updateindex` that controls whether the index\n> > is unlocked or not. We unlock it when the if statement is not taken, and\n> > keep the current codepath when it is. I call it a \"bitter pill\" because\n> > I'm usually not a huge fan of adding knobs like that in functions. Makes\n> > the function harder to reason about and makes it more bug prone.\n> >\n> > If you can think of a better/cleaner way of working around this,\n> > suggestions are welcome!\n> \n> Hmm, so, yeah, the entire if statement only occurs if it can't close\n> the file descriptor. Is that something that actually happens? If so,\n> then it should perhaps be throwing an exception, because having\n> started a rescan is probably more than the caller bargained for. That\n> would prevent the callers from unlocking the index out from under the\n> rescan, and also cancel any other processing they might be doing that\n> is probably making bad assumptions with a rescan running.\n\nThis seems like defensive programming. It is accounting for something \n_really bad_ happening.\n\nIf closing the file descriptor fails, it means the buffer was not \nflushed properly for some reason. Whatever operations we thought we did \nwere potentially not completed. So, we just discard all \nassumptions/state, and get a fresh start by doing a rescan. This was \nintroduced in d4e890e5 (\"git-gui: Make sure we get errors from \ngit-update-index\", 23-10-2007). The commit message says:\n\n    I'm seeing a lot of silent failures from git-update-index on\n    Windows and this is leaving the index.lock file intact, which\n    means users are later unable to perform additional operations.\n\n    When the index is locked behind our back and we are unable to\n    use it we may need to allow the user to delete the index lock\n    and try again.  However our UI state is probably not currect\n    as we have assumed that some changes were applied but none of\n    them actually did.  A rescan is the easiest (in code anyway)\n    solution to correct our UI to show what the index really has\n    (or doesn't have).\n\nSince this is a _really_ old commit, I'm not sure if the problem still \nexists today though.\n\nSo, this recovery code has to go somewhere. Yes, a rescan is certainly \nmore than what the caller wanted, but it is better than working on an \ninconsistent in-memory state of the repo.\n\nThe question then becomes where the best place to do so is. This seems \nlike a good one if we can get our locking requirements to work with it \nproperly.\n\nThe glaring problem is that we don't want the rescan to run while the \ndeletion task is still running because they will interfere with each \nother. Also, deletion expects the index to be locked, so the rescan and \ndeletion should be mutually exclusive.\n\nOne quick hack I can think of is to throw an error from this function, \nand let the caller handle it. Then, in the callers that don't have the \ndeletion task to worry about, they just call the rescan (to be more \nspecific, the body of the if statement - moved to its own function). The \ncallers that do have to worry about the deletion somehow schedule it \nafter the deletion process finished. Or, they somehow cancel the \ndeletion operation, and then run the rescan.\n\nWaiting till the deletion is over can probably be done by polling the \nlock in an `after idle...`.\n\nThis is what I can think of at first glance. Maybe I'm missing a better \nand cleaner way?\n \n> > >       if {$update_index_cp >= $total_cnt} {\n> > > -             _close_updateindex $fd $after\n> > > +             _close_updateindex $fd $do_unlock_index $after\n> >\n> > _close_updateindex takes only one argument, and you pass it 3.\n> > $do_unlock_index does not seem to be defined anywhere. $after is\n> > evaluated just after this line, and _close_updateindex doesn't accept\n> > the argument anyway. I suspect this is a leftover from a different\n> > approach you tried before this one.\n> \n> It is indeed, oops!\n> \n> > Also, unlike all the other places where _close_updateindex is used, this\n> > one does not make a call to unlock_index. Is that intended? IIUC, it\n> > should be intended, since this is the part which uses the \"chord\", but a\n> > confirmation would be nice.\n> \n> Intentional, yes. I'll see if there's a concise way to document this.\n> \n> > > +     # Common \"after\" functionality that waits until multiple asynchronous\n> > > +     # operations are complete (by waiting for them to activate their notes\n> > > +     # on the chord).\n> >\n> > Nitpick: mention what the \"multiple asynchronous operations\" are exactly\n> > (i.e, they are the deletion and index checkout operations).\n> \n> Okeydoke.\n> \n> > >       set after {}\n> >\n> > 'after' seems to be an unused variable. This line can be deleted.\n> \n> Good catch.\n> \n> > > +             if {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n> >\n> > Nitpick: please split the line into two.\n> \n> Will do.\n> \n> > > +                     set error_text \"Encountered errors deleting files:\\n\"\n> >\n> > Wrap the string in a `mc [...]` so it can be translated some time in the\n> > future.\n> \n> Ah, yes, I did that with most messages, this was an oversight.\n> \n> > > +proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n> >\n> > Why use a procedure, and not a global variable? My guess is to make it\n> > impossible for some code to change this value by mistake. Do I guess\n> > correctly?\n> \n> A variable is by definition not a constant. This is the pattern that\n> came up when I did a search for how one makes a constant in Tcl. \"\"\\_(\n> ``_/ )_/\"\"\n> \n> Making it a procedure means that if someone wants to put actual logic\n> behind it in the future, it's already being called as a proc.\n\nMakes sense.\n \n> > Wew! This took longer than I expected ;)\n> >\n> > Tested on Linux. Works fine after fixing the extra arguments passed to\n> > `_close_updateindex`. Thanks.\n> \n> Yeah, I did run things as I was changing them to verify, and felt like\n> I covered everything, I'm surprised I didn't bump into that, obviously\n> I didn't cover everything after all. Perfect demonstration of why\n> developers should never be exclusively responsible for testing their\n> own code :-D\n> \n> Let me know w.r.t. which OO framework to employ and what that means\n> for minimum required versions and/or package references.\n> \n> Thanks very much,\n> \n> Jonathan Gilbert\n\n[0] https://wiki.tcl-lang.org/page/MeTOO\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"386101","messageId":"pull.436.v3.git.1573638988.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v2.git.1573110335.gitgitgadget@gmail.com","subject":"[PATCH v3 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-13T09:56:25Z","receivedAt":"2019-11-13T09:56:36Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nThe _close_updateindex proc contains error handling (added in d4e890e5) that\nhas the potential to interact badly with unlock_index running at the\ncompletion of an async operation. I have refactored the procedure into\nseparate procs _close_updateindex and rescan_on_error, and the original\nfunctionality is captured with _close_updateindex_rescan_on_error. Call\nsites have been updated appropriately, and checkout_index has been reworked\nto take a functor that captures any errors that occur in a caller-defined\nway. revert_helper uses this to supply a lambda function that stashes the\nerror within the chord's body namespace, so that it can call rescan_on_error \nwhen the chord is completed, which might be substantially after \ncheckout_index encounters its error. If a rescan is called for, it is done\nonce the deletion is complete.\n\nThis is the fourth revision of this change, which differs from the third\nversion in the following ways:\n\n * Merged some of the wording from the cover letter into the commit message.\n   \n   \n * The Tcl/Tk dependency is updated to 8.6.\n   \n   \n * The chord.tcl documentation has been reworked, moving the mechanistic\n   class/member documentation next to the declarations, and including a more\n   \"human-readable\" holistic explanation at the top.\n   \n   \n * Class variables defined within chord.tcl have snake_case names. When I\n   made this switch, I saw one possible reason why the convention from the\n   Tcl documentation was different: it's quite common to have a parameter to\n   the constructor with the same name as a class variable, and if the names\n   are literally identical then they conflict in the namespace (e.g. set\n   body $body).\n   \n   \n * Removed unnecessary variable from revert_helper in index.tcl, added\n   comments and split long line per feedback.\n   \n   \n * _close_updateindex's error handling is split out to new proc \n   rescan_on_error, and _close_updateindex_rescan_on_error calls \n   rescan_on_error if it catches an error from _close_updateindex. Most call\n   sites are updated to call the wrapper function, but the split is crucial\n   for error handling within revert_helper.\n   \n   \n * checkout_index uses the _close_updateindex without the error handler, and\n   instead takes a functor from the caller that allows the caller to do\n   whatever it wants with any errors that occur (but without abnormally\n   terminating the execution of checkout_index the way throwing an error\n   would).\n   \n   \n * revert_helper is where the most meaningful change is. The $after_chord \n   body now checks for a stashed error, and if there is one, it calls \n   rescan_on_error. I create a lambda function $capture_error that takes\n   whatever error message is passed to it and stashes it in $after_chord's\n   namespace. Then, this is passed to checkout_index. The result is that if\n   an error happens closing the index, the error message is passed back up,\n   and then the checkout note is activated, but the $after_chord body\n   doesn't execute until the deletion note is also activated, delaying the\n   rescan until after the deletion is complete too.\n   \n   \n\nOn Mon, Nov 11, 2019 at 1:25 PM Pratyush Yadav me@yadavpratyush.com\n[me@yadavpratyush.com] wrote:\n\n> The 'class' documentation [0] suggests adding a \"package require TclOO\". But\nTclOO ships by default with Tcl 8.6 and above. So, I'm not really sure if we\nneed this.\n\n\nI couldn't find any evidence that it is required in my testing.\n\nOn Tue, Nov 12, 2019 at 1:35 PM Pratyush Yadav me@yadavpratyush.com\n[me@yadavpratyush.com] wrote:\n\n> > > > +oo::class create SimpleChord {\n\n\nThis comes from the TclOO package, right?\n\ngit-gui has its own object-oriented system (lib/class.tcl). It was written\ncirca 2007. I suspect something like TclOO did not exist back then.\n\nWhy not use that? Does it have some limitations that TclOO does not have? I\ndo not mind using the \"official\" OO system. I just want to know why exactly\nyou made the choice.\n\n\nHaving limited experience with Tcl, I did a Google search for \"tcl object\noriented\" and ended up writing code using TclOO because that's what came up.\nDo you think it makes sense to rework this to useclass.tcl, or perhaps\ninstead the opposite: have a policy of using the standard TclOO going\nforward, and let the rest of Git Gui organically upgrade itself to some\nhypothetical point in the future where class.tcl is no longer used by\nanything?\n\n\nReplacing class.tcl would be a big effort, and seeing how things stand as of\nnow in terms of active contributors, I don't think it would happen in the\nnear future.\n\nSo the question really boils down to \"do we want to mix these two flavors of\nOO frameworks?\".\n\nIf TclOO gives us some benefit over our homegrown framework, or if our\nframework is in some way hard to use, then I would certainly side on just\nsticking with TclOO.\n\n\nIt looks like the \"treat an object as a method\" functionality that the \nunknown method provides is not easily duplicated with class.tcl. It would be\npossible to just replace it with a named method, but the code using it\nwouldn't look as nice. Also, not that it matters in this instance, but\npurely as a matter of principle, from what I've read, it seems that TclOO is\nsignificantly supported by native code in the runtime and has much better\nperformance and far less overhead than all pre-8.6 OO solutions. This\nsuggests that a long-term goal of eliminating class.tcl might not be a bad\nidea. I haven't seen any way that having chord.tcl use TclOO could interfere\nwith other, unrelated things using class.tcl.\n\n> > > >  * method notify_note_activation {} {\n\n\nSince this method is for internal use only, can it be made \"private\"? Does\nthe OO library support something like this?\n\n\nI don't think so, because it's called from outside the class. What we'd be\nlooking for is something like C++'s \"friend\" syntax. Tcl doesn't seem to\nhave this. Though, I just did some further Googling, and saw a hint that it\nmight be possible to bypass member security on a case-by-case basis, so that\nthe method is private but ChordNote is able to call it anyway. I'll see if I\ncan't figure this out. :-)\n\n\nI don't think too much complexity/hacking is warranted for something like\nthis. If you can figure out a really simple way to do it, great! Otherwise,\njust keep it like it is.\n\n\nIt seems that there isn't any way in TclOO to get into a class without it\nhaving a public \"door\", whether it's the method itself, or some \"accessor\"\nmethod that returns its [my] functor. You can choose to unexport a method,\nbut once you do that, there doesn't appear to exist any way at all to\noverride this for just one call site.\n\n> > \"This is a mechanism for making an object that can be called like a method.\"\n\n\nLooks like this method would also be called if someone misspelled a method\nname for this object. So say if someone by mistake writes\n\n $note is_activate\n\nthis method would be called. This is a clear bug. So, add a check here to\nmake sure 'methodName' is actually absent. And if it isn't, display an\nerror. Displaying an error to the user on a programmer error can get\nannoying. But since we don't have something like assertions in git-gui yet,\nmaybe that's the best way to get bugs noticed.\n\n\nI did some testing and discovered that I was mistaken, if unknown has no\nparameters then it cannot receive calls to unknown method names, these\ngenerate errors. As written, it is only capable of processing calls against\nthe object itself.\n\n> > Hmm, so, yeah, the entire if statement only occurs if it can't close the\nfile descriptor. Is that something that actually happens? If so, then it\nshould perhaps be throwing an exception, because having started a rescan is\nprobably more than the caller bargained for. That would prevent the callers\nfrom unlocking the index out from under the rescan, and also cancel any\nother processing they might be doing that is probably making bad assumptions\nwith a rescan running.\n\n\nThis seems like defensive programming. It is accounting for somethingreally\nbad happening.\n\n\n[..]\n\n> So, this recovery code has to go somewhere. Yes, a rescan is certainly more\nthan what the caller wanted, but it is better than working on an\ninconsistent in-memory state of the repo.\n\nThe question then becomes where the best place to do so is. This seems like\na good one if we can get our locking requirements to work with it properly.>\nThe glaring problem is that we don't want the rescan to run while the\ndeletion task is still running because they will interfere with each other.\nAlso, deletion expects the index to be locked, so the rescan and deletion\nshould be mutually exclusive.\n\n\nI came up with a fairly concise way to defer the rescan until all async\noperations are completed, by splitting the error handling out into a\nseparate method and then making this flow call that method from the\n$after_chord body.\n\nJonathan Gilbert (2):\n  git-gui: consolidate naming conventions\n  git-gui: revert untracked files by deleting them\n\n git-gui.sh    |   4 +-\n lib/chord.tcl | 160 ++++++++++++++++\n lib/index.tcl | 500 +++++++++++++++++++++++++++++++++++++-------------\n 3 files changed, 538 insertions(+), 126 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v2:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n 2:  9469beb599 ! 2:  dc12c1668d git-gui: revert untracked files by deleting them\n     @@ -2,66 +2,72 @@\n      \n          git-gui: revert untracked files by deleting them\n      \n     -    Update the revert_helper procedure to also detect untracked files. If\n     -    files are present, the user is asked if they want them deleted. Perform\n     -    the deletion in batches, using new proc delete_files with helper\n     -    delete_helper, to allow the UI to remain responsive. Coordinate the\n     -    completion of multiple overlapping asynchronous operations using a new\n     -    construct called a \"chord\". Migrate unlocking of the index out of\n     -    _close_updateindex to a responsibility of the caller, to permit paths\n     -    that don't directly unlock the index.\n     +    Update the revert_helper proc to check for untracked files as well as\n     +    changes, and then handle changes to be reverted and untracked files with\n     +    independent blocks of code. Prompt the user independently for untracked\n     +    files, since the underlying action is fundamentally different (rm -f).\n     +    If after deleting untracked files, the directory containing them becomes\n     +    empty, then remove the directory as well. Migrate unlocking of the index\n     +    out of _close_updateindex to a responsibility of the caller, to permit\n     +    paths that don't directly unlock the index, and refactor the error\n     +    handling added in d4e890e5 so that callers can make flow control\n     +    decisions in the event of errors.\n     +\n     +    A new proc delete_files takes care of actually deleting the files in\n     +    batches, using the Tcler's Wiki recommended approach for keeping the UI\n     +    responsive.\n     +\n     +    Since the checkout_index and delete_files calls are both asynchronous\n     +    and could potentially complete in any order, a \"chord\" is used to\n     +    coordinate unlocking the index and returning the UI to a usable state\n     +    only after both operations are complete. The `SimpleChord` class,\n     +    based on TclOO (Tcl/Tk 8.6), is added in this commit.\n      \n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n     + diff --git a/git-gui.sh b/git-gui.sh\n     + --- a/git-gui.sh\n     + +++ b/git-gui.sh\n     +@@\n     + ##\n     + ## Tcl/Tk sanity check\n     + \n     +-if {[catch {package require Tcl 8.4} err]\n     +- || [catch {package require Tk  8.4} err]\n     ++if {[catch {package require Tcl 8.6} err]\n     ++ || [catch {package require Tk  8.6} err]\n     + } {\n     + \tcatch {wm withdraw .}\n     + \ttk_messageBox \\\n     +\n       diff --git a/lib/chord.tcl b/lib/chord.tcl\n       new file mode 100644\n       --- /dev/null\n       +++ b/lib/chord.tcl\n      @@\n     -+# SimpleChord class:\n     -+#   Represents a procedure that conceptually has multiple entrypoints that must\n     -+#   all be called before the procedure executes. Each entrypoint is called a\n     -+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n     -+#\n     -+#   Constructor:\n     -+#     set chord [SimpleChord new {body}]\n     -+#       Creates a new chord object with the specified body script. The body\n     -+#       script is evaluated at most once, when a note is activated and the\n     -+#       chord has no other non-activated notes.\n     -+#\n     -+#   Methods:\n     -+#     $chord eval {script}\n     -+#       Runs the specified script in the same context (namespace) in which the\n     -+#       chord body will be evaluated. This can be used to set variable values\n     -+#       for the chord body to use.\n     -+#\n     -+#     set note [$chord add_note]\n     -+#       Adds a new note to the chord, an instance of ChordNote. Raises an\n     -+#       error if the chord is already completed, otherwise the chord is updated\n     -+#       so that the new note must also be activated before the body is\n     -+#       evaluated.\n     -+#\n     -+#     $chord notify_note_activation\n     -+#       For internal use only.\n     -+#\n     -+# ChordNote class:\n     -+#   Represents a note within a chord, providing a way to activate it. When the\n     -+#   final note of the chord is activated (this can be any note in the chord,\n     -+#   with all other notes already previously activated in any order), the chord's\n     -+#   body is evaluated.\n     ++# Simple Chord for Tcl\n      +#\n     -+#   Constructor:\n     -+#     Instances of ChordNote are created internally by calling add_note on\n     -+#     SimpleChord objects.\n     ++# A \"chord\" is a method with more than one entrypoint and only one body, such\n     ++# that the body runs only once all the entrypoints have been called by\n     ++# different asynchronous tasks. In this implementation, the chord is defined\n     ++# dynamically for each invocation. A SimpleChord object is created, supplying\n     ++# body script to be run when the chord is completed, and then one or more notes\n     ++# are added to the chord. Each note can be called like a proc, and returns\n     ++# immediately if the chord isn't yet complete. When the last remaining note is\n     ++# called, the body runs before the note returns.\n      +#\n     -+#   Methods:\n     -+#     [$note is_activated]\n     -+#       Returns true if this note has already been activated.\n     ++# The SimpleChord class has a constructor that takes the body script, and a\n     ++# method add_note that returns a note object. Since the body script does not\n     ++# run in the context of the procedure that defined it, a mechanism is provided\n     ++# for injecting variables into the chord for use by the body script. The\n     ++# activation of a note is idempotent; multiple calls have the same effect as\n     ++# a simple call.\n      +#\n     -+#     $note\n     -+#       Activates the note, if it has not already been activated, and completes\n     -+#       the chord if there are no other notes awaiting activation. Subsequent\n     -+#       calls will have no further effect.\n     ++# If you are invoking asynchronous operations with chord notes as completion\n     ++# callbacks, and there is a possibility that earlier operations could complete\n     ++# before later ones are started, it is a good practice to create a \"common\"\n     ++# note on the chord that prevents it from being complete until you're certain\n     ++# you've added all the notes you need.\n      +#\n      +# Example:\n      +#\n     @@ -97,61 +103,105 @@\n      +# last one, completing the chord before this code finishes, or anything in\n      +# between. The purpose of the chord is to not have to worry about the order.\n      +\n     ++# SimpleChord class:\n     ++#   Represents a procedure that conceptually has multiple entrypoints that must\n     ++#   all be called before the procedure executes. Each entrypoint is called a\n     ++#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n      +oo::class create SimpleChord {\n     -+\tvariable Notes\n     -+\tvariable Body\n     -+\tvariable IsCompleted\n     ++\tvariable notes body is_completed\n      +\n     ++\t# Constructor:\n     ++\t#   set chord [SimpleChord new {body}]\n     ++\t#     Creates a new chord object with the specified body script. The\n     ++\t#     body script is evaluated at most once, when a note is activated\n     ++\t#     and the chord has no other non-activated notes.\n      +\tconstructor {body} {\n     -+\t\tset Notes [list]\n     -+\t\tset Body $body\n     -+\t\tset IsCompleted 0\n     ++\t\tset notes [list]\n     ++\t\tmy eval [list set body $body]\n     ++\t\tset is_completed 0\n      +\t}\n      +\n     ++\t# Method:\n     ++\t#   $chord eval {script}\n     ++\t#     Runs the specified script in the same context (namespace) in which\n     ++\t#     the chord body will be evaluated. This can be used to set variable\n     ++\t#     values for the chord body to use.\n      +\tmethod eval {script} {\n      +\t\tnamespace eval [self] $script\n      +\t}\n      +\n     ++\t# Method:\n     ++\t#   set note [$chord add_note]\n     ++\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n     ++\t#     error if the chord is already completed, otherwise the chord is\n     ++\t#     updated so that the new note must also be activated before the\n     ++\t#     body is evaluated.\n      +\tmethod add_note {} {\n     -+\t\tif {$IsCompleted} { error \"Cannot add a note to a completed chord\" }\n     ++\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n      +\n      +\t\tset note [ChordNote new [self]]\n      +\n     -+\t\tlappend Notes $note\n     ++\t\tlappend notes $note\n      +\n      +\t\treturn $note\n      +\t}\n      +\n     ++\t# This method is for internal use only and is intentionally undocumented.\n      +\tmethod notify_note_activation {} {\n     -+\t\tif {!$IsCompleted} {\n     -+\t\t\tforeach note $Notes {\n     ++\t\tif {!$is_completed} {\n     ++\t\t\tforeach note $notes {\n      +\t\t\t\tif {![$note is_activated]} { return }\n      +\t\t\t}\n      +\n     -+\t\t\tset IsCompleted 1\n     ++\t\t\tset is_completed 1\n      +\n     -+\t\t\tnamespace eval [self] $Body\n     ++\t\t\tnamespace eval [self] $body\n      +\t\t\tnamespace delete [self]\n      +\t\t}\n      +\t}\n      +}\n      +\n     ++# ChordNote class:\n     ++#   Represents a note within a chord, providing a way to activate it. When the\n     ++#   final note of the chord is activated (this can be any note in the chord,\n     ++#   with all other notes already previously activated in any order), the chord's\n     ++#   body is evaluated.\n      +oo::class create ChordNote {\n     -+\tvariable Chord IsActivated\n     ++\tvariable chord is_activated\n      +\n     ++\t# Constructor:\n     ++\t#   Instances of ChordNote are created internally by calling add_note on\n     ++\t#   SimpleChord objects.\n      +\tconstructor {chord} {\n     -+\t\tset Chord $chord\n     -+\t\tset IsActivated 0\n     ++\t\tmy eval set chord $chord\n     ++\t\tset is_activated 0\n      +\t}\n      +\n     ++\t# Method:\n     ++\t#   [$note is_activated]\n     ++\t#     Returns true if this note has already been activated.\n      +\tmethod is_activated {} {\n     -+\t\treturn $IsActivated\n     ++\t\treturn $is_activated\n      +\t}\n      +\n     ++\t# Method:\n     ++\t#   $note\n     ++\t#     Activates the note, if it has not already been activated, and\n     ++\t#     completes the chord if there are no other notes awaiting\n     ++\t#     activation. Subsequent calls will have no further effect.\n     ++\t#\n     ++\t# NB: In TclOO, if an object is invoked like a method without supplying\n     ++\t#     any method name, then this internal method `unknown` is what\n     ++\t#     actually runs (with no parameters). It is used in the ChordNote\n     ++\t#     class for the purpose of allowing the note object to be called as\n     ++\t#     a function (see example above). (The `unknown` method can also be\n     ++\t#     used to support dynamic dispatch, but must take parameters to\n     ++\t#     identify the \"unknown\" method to be invoked. In this form, this\n     ++\t#     proc serves only to make instances behave directly like methods.)\n      +\tmethod unknown {} {\n     -+\t\tif {!$IsActivated} {\n     -+\t\t\tset IsActivated 1\n     -+\t\t\t$Chord notify_note_activation\n     ++\t\tif {!$is_activated} {\n     ++\t\t\tset is_activated 1\n     ++\t\t\t$chord notify_note_activation\n      +\t\t}\n      +\t}\n      +}\n     @@ -164,16 +214,103 @@\n       }\n       \n      -proc _close_updateindex {fd after} {\n     +-\tglobal use_ttk NS\n     ++# Returns true if the operation succeeded, false if a rescan has been initiated.\n     ++proc _close_updateindex_rescan_on_error {fd} {\n     ++\tif {![catch {_close_updateindex $fd} err]} {\n     ++\t\treturn true\n     ++\t} else {\n     ++\t\trescan_on_error $err\n     ++\t\treturn false\n     ++\t}\n     ++}\n     ++\n      +proc _close_updateindex {fd} {\n     - \tglobal use_ttk NS\n       \tfconfigure $fd -blocking 1\n     - \tif {[catch {close $fd} err]} {\n     -@@\n     - \t}\n     +-\tif {[catch {close $fd} err]} {\n     +-\t\tset w .indexfried\n     +-\t\tDialog $w\n     +-\t\twm withdraw $w\n     +-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n     +-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n     +-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n     +-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n     +-\t\t\t-width [string length $s] -relief flat \\\n     +-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n     +-\t\t\t-background [get_bg_color $w]\n     +-\t\t$w.msg tag configure bold -font font_uibold -justify center\n     +-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n     +-\t\t$w.msg insert end $s bold \\n\\n$err {}\n     +-\t\t$w.msg configure -state disabled\n     +-\n     +-\t\t${NS}::button $w.continue \\\n     +-\t\t\t-text [mc \"Continue\"] \\\n     +-\t\t\t-command [list destroy $w]\n     +-\t\t${NS}::button $w.unlock \\\n     +-\t\t\t-text [mc \"Unlock Index\"] \\\n     +-\t\t\t-command \"destroy $w; _delete_indexlock\"\n     +-\t\tgrid $w.msg - $w.vs -sticky news\n     +-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n     +-\t\tgrid columnconfigure $w 0 -weight 1\n     +-\t\tgrid rowconfigure $w 0 -weight 1\n     +-\n     +-\t\twm protocol $w WM_DELETE_WINDOW update\n     +-\t\tbind $w.continue <Visibility> \"\n     +-\t\t\tgrab $w\n     +-\t\t\tfocus %W\n     +-\t\t\"\n     +-\t\twm deiconify $w\n     +-\t\ttkwait window $w\n     ++\tclose $fd\n     ++\t$::main_status stop\n     ++}\n     + \n     +-\t\t$::main_status stop\n     +-\t\tunlock_index\n     +-\t\trescan $after 0\n     +-\t\treturn\n     +-\t}\n     ++proc rescan_on_error {err} {\n     ++\tglobal use_ttk NS\n     ++\n     ++\tset w .indexfried\n     ++\tDialog $w\n     ++\twm withdraw $w\n     ++\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n     ++\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n     ++\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n     ++\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n     ++\t\t-width [string length $s] -relief flat \\\n     ++\t\t-borderwidth 0 -highlightthickness 0 \\\n     ++\t\t-background [get_bg_color $w]\n     ++\t$w.msg tag configure bold -font font_uibold -justify center\n     ++\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n     ++\t$w.msg insert end $s bold \\n\\n$err {}\n     ++\t$w.msg configure -state disabled\n     ++\n     ++\t${NS}::button $w.continue \\\n     ++\t\t-text [mc \"Continue\"] \\\n     ++\t\t-command [list destroy $w]\n     ++\t${NS}::button $w.unlock \\\n     ++\t\t-text [mc \"Unlock Index\"] \\\n     ++\t\t-command \"destroy $w; _delete_indexlock\"\n     ++\tgrid $w.msg - $w.vs -sticky news\n     ++\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n     ++\tgrid columnconfigure $w 0 -weight 1\n     ++\tgrid rowconfigure $w 0 -weight 1\n     ++\n     ++\twm protocol $w WM_DELETE_WINDOW update\n     ++\tbind $w.continue <Visibility> \"\n     ++\t\tgrab $w\n     ++\t\tfocus %W\n     ++\t\"\n     ++\twm deiconify $w\n     ++\ttkwait window $w\n       \n       \t$::main_status stop\n     --\tunlock_index\n     + \tunlock_index\n      -\tuplevel #0 $after\n     ++\trescan ui_ready 0\n       }\n       \n       proc update_indexinfo {msg path_list after} {\n     @@ -182,8 +319,10 @@\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     -+\t\t_close_updateindex $fd\n     -+\t\tunlock_index\n     ++\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n     ++\t\t\tunlock_index\n     ++\t\t}\n     ++\n      +\t\tuplevel #0 $after\n       \t\treturn\n       \t}\n     @@ -193,37 +332,86 @@\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     -+\t\t_close_updateindex $fd\n     -+\t\tunlock_index\n     ++\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n     ++\t\t\tunlock_index\n     ++\t\t}\n     ++\n      +\t\tuplevel #0 $after\n       \t\treturn\n       \t}\n       \n      @@\n     + \t$::main_status update $update_index_cp $total_cnt\n     + }\n     + \n     +-proc checkout_index {msg path_list after} {\n     ++proc checkout_index {msg path_list after capture_error} {\n     + \tglobal update_index_cp\n     + \n     + \tif {![lock_index update]} return\n     +@@\n     + \t\t$total_cnt \\\n     + \t\t$batch \\\n     + \t\t$after \\\n     ++\t\t$capture_error \\\n     + \t\t]\n     + }\n     + \n     +-proc write_checkout_index {fd path_list total_cnt batch after} {\n     ++proc write_checkout_index {fd path_list total_cnt batch after capture_error} {\n     + \tglobal update_index_cp\n       \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     -+\t\t_close_updateindex $fd $do_unlock_index $after\n     ++\t\tif {[catch {_close_updateindex $fd} err]} {\n     ++\t\t\tuplevel #0 $capture_error [list $err]\n     ++\t\t}\n     ++\n      +\t\tuplevel #0 $after\n     ++\n       \t\treturn\n       \t}\n       \n      @@\n     + \tadd_helper [mc \"Adding all changed files\"] $paths\n     + }\n     + \n     ++# Copied from TclLib package \"lambda\".\n     ++proc lambda {arguments body args} {\n     ++\treturn [list ::apply [list $arguments $body] {*}$args]\n     ++}\n     ++\n     + proc revert_helper {txt paths} {\n     + \tglobal file_states current_diff_path\n       \n       \tif {![lock_index begin-update]} return\n       \n      +\t# Common \"after\" functionality that waits until multiple asynchronous\n      +\t# operations are complete (by waiting for them to activate their notes\n      +\t# on the chord).\n     ++\t#\n     ++\t# The asynchronous operations are each indicated below by a comment\n     ++\t# before the code block that starts the async operation.\n      +\tset after_chord [SimpleChord new {\n      +\t\tunlock_index\n     -+\t\tif {$should_reshow_diff} { reshow_diff }\n     -+\t\tui_ready\n     ++\t\tif {[string trim $err] != \"\"} {\n     ++\t\t\trescan_on_error $err\n     ++\t\t} else {\n     ++\t\t\tif {$should_reshow_diff} { reshow_diff }\n     ++\t\t\tui_ready\n     ++\t\t}\n      +\t}]\n      +\n      +\t$after_chord eval { set should_reshow_diff 0 }\n      +\n     ++\t# This function captures an error for processing when after_chord is\n     ++\t# completed. (The chord is curried into the lambda function.)\n     ++\tset capture_error [lambda \\\n     ++\t\t{chord error} \\\n     ++\t\t{ $chord eval [list set err $error] } \\\n     ++\t\t$after_chord]\n     ++\n      +\t# We don't know how many notes we're going to create (it's dynamic based\n      +\t# on conditional paths below), so create a common note that will delay\n      +\t# the chord's completion until we activate it, and then activate it\n     @@ -231,8 +419,9 @@\n      +\tset after_common_note [$after_chord add_note]\n      +\n       \tset path_list [list]\n     +-\tset after {}\n      +\tset untracked_list [list]\n     - \tset after {}\n     ++\n       \tforeach path $paths {\n       \t\tswitch -glob -- [lindex $file_states($path) 0] {\n       \t\tU? {continue}\n     @@ -253,26 +442,9 @@\n       \n      +\tset path_cnt [llength $path_list]\n      +\tset untracked_cnt [llength $untracked_list]\n     - \n     --\t# Split question between singular and plural cases, because\n     --\t# such distinction is needed in some languages. Previously, the\n     --\t# code used \"Revert changes in\" for both, but that can't work\n     --\t# in languages where 'in' must be combined with word from\n     --\t# rest of string (in different way for both cases of course).\n     --\t#\n     --\t# FIXME: Unfortunately, even that isn't enough in some languages\n     --\t# as they have quite complex plural-form rules. Unfortunately,\n     --\t# msgcat doesn't seem to support that kind of string translation.\n     --\t#\n     --\tset n [llength $path_list]\n     --\tif {$n == 0} {\n     --\t\tunlock_index\n     --\t\treturn\n     --\t} elseif {$n == 1} {\n     --\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n     --\t} else {\n     --\t\tset query [mc \"Revert changes in these %i files?\" $n]\n     --\t}\n     ++\n     ++\t# Asynchronous operation: revert changes by checking them out afresh\n     ++\t# from the index.\n      +\tif {$path_cnt > 0} {\n      +\t\t# Split question between singular and plural cases, because\n      +\t\t# such distinction is needed in some languages. Previously, the\n     @@ -295,25 +467,31 @@\n      +\t\t\t\t\"Revert changes in these %i files?\" \\\n      +\t\t\t\t$path_cnt]\n      +\t\t}\n     - \n     --\tset reply [tk_dialog \\\n     --\t\t.confirm_revert \\\n     --\t\t\"[appname] ([reponame])\" \\\n     --\t\t\"$query\n     ++\n      +\t\tset reply [tk_dialog \\\n      +\t\t\t.confirm_revert \\\n      +\t\t\t\"[appname] ([reponame])\" \\\n      +\t\t\t\"$query\n       \n     - [mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n     --\t\tquestion \\\n     --\t\t1 \\\n     --\t\t[mc \"Do Nothing\"] \\\n     --\t\t[mc \"Revert Changes\"] \\\n     --\t\t]\n     --\tif {$reply == 1} {\n     --\t\tcheckout_index \\\n     --\t\t\t$txt \\\n     +-\t# Split question between singular and plural cases, because\n     +-\t# such distinction is needed in some languages. Previously, the\n     +-\t# code used \"Revert changes in\" for both, but that can't work\n     +-\t# in languages where 'in' must be combined with word from\n     +-\t# rest of string (in different way for both cases of course).\n     +-\t#\n     +-\t# FIXME: Unfortunately, even that isn't enough in some languages\n     +-\t# as they have quite complex plural-form rules. Unfortunately,\n     +-\t# msgcat doesn't seem to support that kind of string translation.\n     +-\t#\n     +-\tset n [llength $path_list]\n     +-\tif {$n == 0} {\n     +-\t\tunlock_index\n     +-\t\treturn\n     +-\t} elseif {$n == 1} {\n     +-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n     +-\t} else {\n     +-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n     ++[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n      +\t\t\tquestion \\\n      +\t\t\t1 \\\n      +\t\t\t[mc \"Do Nothing\"] \\\n     @@ -324,10 +502,16 @@\n      +\t\t\tcheckout_index \\\n      +\t\t\t\t$txt \\\n      +\t\t\t\t$path_list \\\n     -+\t\t\t\t[$after_chord add_note]\n     ++\t\t\t\t[$after_chord add_note] \\\n     ++\t\t\t\t$capture_error\n      +\t\t}\n     -+\t}\n     -+\n     + \t}\n     + \n     +-\tset reply [tk_dialog \\\n     +-\t\t.confirm_revert \\\n     +-\t\t\"[appname] ([reponame])\" \\\n     +-\t\t\"$query\n     ++\t# Asynchronous operation: Deletion of untracked files.\n      +\tif {$untracked_cnt > 0} {\n      +\t\t# Split question between singular and plural cases, because\n      +\t\t# such distinction is needed in some languages.\n     @@ -348,7 +532,16 @@\n      +\t\t\t\t$untracked_cnt \\\n      +\t\t\t\t]\n      +\t\t}\n     -+\n     + \n     +-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n     +-\t\tquestion \\\n     +-\t\t1 \\\n     +-\t\t[mc \"Do Nothing\"] \\\n     +-\t\t[mc \"Revert Changes\"] \\\n     +-\t\t]\n     +-\tif {$reply == 1} {\n     +-\t\tcheckout_index \\\n     +-\t\t\t$txt \\\n      +\t\tset reply [tk_dialog \\\n      +\t\t\t.confirm_revert \\\n      +\t\t\t\"[appname] ([reponame])\" \\\n     @@ -453,8 +646,9 @@\n      +\t\t# Report error, if any, based on how many deletions failed.\n      +\t\tset deletion_error_cnt [llength $deletion_errors]\n      +\n     -+\t\tif {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n     -+\t\t\tset error_text \"Encountered errors deleting files:\\n\"\n     ++\t\tif {($deletion_error_cnt > 0)\n     ++\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n     ++\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n      +\n      +\t\t\tforeach deletion_error $deletion_errors {\n      +\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n\n-- \ngitgitgadget\n"},{"id":"386102","messageId":"dc12c1668dce875c99a45fb49ad5854a13ef4f35.1573638988.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v3.git.1573638988.gitgitgadget@gmail.com","subject":"[PATCH v3 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-13T09:56:27Z","receivedAt":"2019-11-13T09:56:38Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper proc to check for untracked files as well as\nchanges, and then handle changes to be reverted and untracked files with\nindependent blocks of code. Prompt the user independently for untracked\nfiles, since the underlying action is fundamentally different (rm -f).\nIf after deleting untracked files, the directory containing them becomes\nempty, then remove the directory as well. Migrate unlocking of the index\nout of _close_updateindex to a responsibility of the caller, to permit\npaths that don't directly unlock the index, and refactor the error\nhandling added in d4e890e5 so that callers can make flow control\ndecisions in the event of errors.\n\nA new proc delete_files takes care of actually deleting the files in\nbatches, using the Tcler's Wiki recommended approach for keeping the UI\nresponsive.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand could potentially complete in any order, a \"chord\" is used to\ncoordinate unlocking the index and returning the UI to a usable state\nonly after both operations are complete. The `SimpleChord` class,\nbased on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh    |   4 +-\n lib/chord.tcl | 160 +++++++++++++++++++\n lib/index.tcl | 416 ++++++++++++++++++++++++++++++++++++++++----------\n 3 files changed, 496 insertions(+), 84 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex 0d21f5688b..dd6f3a3592 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n ##\n ## Tcl/Tk sanity check\n \n-if {[catch {package require Tcl 8.4} err]\n- || [catch {package require Tk  8.4} err]\n+if {[catch {package require Tcl 8.6} err]\n+ || [catch {package require Tk  8.6} err]\n } {\n \tcatch {wm withdraw .}\n \ttk_messageBox \\\ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..275a6cd4a1\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,160 @@\n+# Simple Chord for Tcl\n+#\n+# A \"chord\" is a method with more than one entrypoint and only one body, such\n+# that the body runs only once all the entrypoints have been called by\n+# different asynchronous tasks. In this implementation, the chord is defined\n+# dynamically for each invocation. A SimpleChord object is created, supplying\n+# body script to be run when the chord is completed, and then one or more notes\n+# are added to the chord. Each note can be called like a proc, and returns\n+# immediately if the chord isn't yet complete. When the last remaining note is\n+# called, the body runs before the note returns.\n+#\n+# The SimpleChord class has a constructor that takes the body script, and a\n+# method add_note that returns a note object. Since the body script does not\n+# run in the context of the procedure that defined it, a mechanism is provided\n+# for injecting variables into the chord for use by the body script. The\n+# activation of a note is idempotent; multiple calls have the same effect as\n+# a simple call.\n+#\n+# If you are invoking asynchronous operations with chord notes as completion\n+# callbacks, and there is a possibility that earlier operations could complete\n+# before later ones are started, it is a good practice to create a \"common\"\n+# note on the chord that prevents it from being complete until you're certain\n+# you've added all the notes you need.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+oo::class create SimpleChord {\n+\tvariable notes body is_completed\n+\n+\t# Constructor:\n+\t#   set chord [SimpleChord new {body}]\n+\t#     Creates a new chord object with the specified body script. The\n+\t#     body script is evaluated at most once, when a note is activated\n+\t#     and the chord has no other non-activated notes.\n+\tconstructor {body} {\n+\t\tset notes [list]\n+\t\tmy eval [list set body $body]\n+\t\tset is_completed 0\n+\t}\n+\n+\t# Method:\n+\t#   $chord eval {script}\n+\t#     Runs the specified script in the same context (namespace) in which\n+\t#     the chord body will be evaluated. This can be used to set variable\n+\t#     values for the chord body to use.\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\t# Method:\n+\t#   set note [$chord add_note]\n+\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n+\t#     error if the chord is already completed, otherwise the chord is\n+\t#     updated so that the new note must also be activated before the\n+\t#     body is evaluated.\n+\tmethod add_note {} {\n+\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\t# This method is for internal use only and is intentionally undocumented.\n+\tmethod notify_note_activation {} {\n+\t\tif {!$is_completed} {\n+\t\t\tforeach note $notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset is_completed 1\n+\n+\t\t\tnamespace eval [self] $body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+oo::class create ChordNote {\n+\tvariable chord is_activated\n+\n+\t# Constructor:\n+\t#   Instances of ChordNote are created internally by calling add_note on\n+\t#   SimpleChord objects.\n+\tconstructor {chord} {\n+\t\tmy eval set chord $chord\n+\t\tset is_activated 0\n+\t}\n+\n+\t# Method:\n+\t#   [$note is_activated]\n+\t#     Returns true if this note has already been activated.\n+\tmethod is_activated {} {\n+\t\treturn $is_activated\n+\t}\n+\n+\t# Method:\n+\t#   $note\n+\t#     Activates the note, if it has not already been activated, and\n+\t#     completes the chord if there are no other notes awaiting\n+\t#     activation. Subsequent calls will have no further effect.\n+\t#\n+\t# NB: In TclOO, if an object is invoked like a method without supplying\n+\t#     any method name, then this internal method `unknown` is what\n+\t#     actually runs (with no parameters). It is used in the ChordNote\n+\t#     class for the purpose of allowing the note object to be called as\n+\t#     a function (see example above). (The `unknown` method can also be\n+\t#     used to support dynamic dispatch, but must take parameters to\n+\t#     identify the \"unknown\" method to be invoked. In this form, this\n+\t#     proc serves only to make instances behave directly like methods.)\n+\tmethod unknown {} {\n+\t\tif {!$is_activated} {\n+\t\t\tset is_activated 1\n+\t\t\t$chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..3ac08281c2 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,53 +7,62 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n-\tglobal use_ttk NS\n+# Returns true if the operation succeeded, false if a rescan has been initiated.\n+proc _close_updateindex_rescan_on_error {fd} {\n+\tif {![catch {_close_updateindex $fd} err]} {\n+\t\treturn true\n+\t} else {\n+\t\trescan_on_error $err\n+\t\treturn false\n+\t}\n+}\n+\n+proc _close_updateindex {fd} {\n \tfconfigure $fd -blocking 1\n-\tif {[catch {close $fd} err]} {\n-\t\tset w .indexfried\n-\t\tDialog $w\n-\t\twm withdraw $w\n-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n-\t\t\t-width [string length $s] -relief flat \\\n-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n-\t\t\t-background [get_bg_color $w]\n-\t\t$w.msg tag configure bold -font font_uibold -justify center\n-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n-\t\t$w.msg insert end $s bold \\n\\n$err {}\n-\t\t$w.msg configure -state disabled\n-\n-\t\t${NS}::button $w.continue \\\n-\t\t\t-text [mc \"Continue\"] \\\n-\t\t\t-command [list destroy $w]\n-\t\t${NS}::button $w.unlock \\\n-\t\t\t-text [mc \"Unlock Index\"] \\\n-\t\t\t-command \"destroy $w; _delete_indexlock\"\n-\t\tgrid $w.msg - $w.vs -sticky news\n-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n-\t\tgrid columnconfigure $w 0 -weight 1\n-\t\tgrid rowconfigure $w 0 -weight 1\n-\n-\t\twm protocol $w WM_DELETE_WINDOW update\n-\t\tbind $w.continue <Visibility> \"\n-\t\t\tgrab $w\n-\t\t\tfocus %W\n-\t\t\"\n-\t\twm deiconify $w\n-\t\ttkwait window $w\n+\tclose $fd\n+\t$::main_status stop\n+}\n \n-\t\t$::main_status stop\n-\t\tunlock_index\n-\t\trescan $after 0\n-\t\treturn\n-\t}\n+proc rescan_on_error {err} {\n+\tglobal use_ttk NS\n+\n+\tset w .indexfried\n+\tDialog $w\n+\twm withdraw $w\n+\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n+\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n+\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n+\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n+\t\t-width [string length $s] -relief flat \\\n+\t\t-borderwidth 0 -highlightthickness 0 \\\n+\t\t-background [get_bg_color $w]\n+\t$w.msg tag configure bold -font font_uibold -justify center\n+\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n+\t$w.msg insert end $s bold \\n\\n$err {}\n+\t$w.msg configure -state disabled\n+\n+\t${NS}::button $w.continue \\\n+\t\t-text [mc \"Continue\"] \\\n+\t\t-command [list destroy $w]\n+\t${NS}::button $w.unlock \\\n+\t\t-text [mc \"Unlock Index\"] \\\n+\t\t-command \"destroy $w; _delete_indexlock\"\n+\tgrid $w.msg - $w.vs -sticky news\n+\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n+\tgrid columnconfigure $w 0 -weight 1\n+\tgrid rowconfigure $w 0 -weight 1\n+\n+\twm protocol $w WM_DELETE_WINDOW update\n+\tbind $w.continue <Visibility> \"\n+\t\tgrab $w\n+\t\tfocus %W\n+\t\"\n+\twm deiconify $w\n+\ttkwait window $w\n \n \t$::main_status stop\n \tunlock_index\n-\tuplevel #0 $after\n+\trescan ui_ready 0\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -90,7 +99,11 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n+\t\t\tunlock_index\n+\t\t}\n+\n+\t\tuplevel #0 $after\n \t\treturn\n \t}\n \n@@ -156,7 +169,11 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n+\t\t\tunlock_index\n+\t\t}\n+\n+\t\tuplevel #0 $after\n \t\treturn\n \t}\n \n@@ -193,7 +210,7 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg path_list after} {\n+proc checkout_index {msg path_list after capture_error} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n@@ -225,15 +242,21 @@ proc checkout_index {msg path_list after} {\n \t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n+\t\t$capture_error \\\n \t\t]\n }\n \n-proc write_checkout_index {fd path_list total_cnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after capture_error} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\tif {[catch {_close_updateindex $fd} err]} {\n+\t\t\tuplevel #0 $capture_error [list $err]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\n \t\treturn\n \t}\n \n@@ -388,66 +411,295 @@ proc do_add_all {} {\n \tadd_helper [mc \"Adding all changed files\"] $paths\n }\n \n+# Copied from TclLib package \"lambda\".\n+proc lambda {arguments body args} {\n+\treturn [list ::apply [list $arguments $body] {*}$args]\n+}\n+\n proc revert_helper {txt paths} {\n \tglobal file_states current_diff_path\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\t#\n+\t# The asynchronous operations are each indicated below by a comment\n+\t# before the code block that starts the async operation.\n+\tset after_chord [SimpleChord new {\n+\t\tunlock_index\n+\t\tif {[string trim $err] != \"\"} {\n+\t\t\trescan_on_error $err\n+\t\t} else {\n+\t\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\t\tui_ready\n+\t\t}\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# This function captures an error for processing when after_chord is\n+\t# completed. (The chord is curried into the lambda function.)\n+\tset capture_error [lambda \\\n+\t\t{chord error} \\\n+\t\t{ $chord eval [list set err $error] } \\\n+\t\t$after_chord]\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n-\tset after {}\n+\tset untracked_list [list]\n+\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n+\n+\t# Asynchronous operation: revert changes by checking them out afresh\n+\t# from the index.\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n+\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n+[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note] \\\n+\t\t\t\t$capture_error\n+\t\t}\n \t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t# Asynchronous operation: Deletion of untracked files.\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n \n-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$::main_status update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$::main_status stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0)\n+\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"386103","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1573638988.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v3.git.1573638988.gitgitgadget@gmail.com","subject":"[PATCH v3 1/2] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-13T09:56:26Z","receivedAt":"2019-11-13T09:56:38Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"386370","messageId":"20191116151113.mwbaendh6lgykfw3@yadavpratyush.com","threadId":"52154","inReplyTo":"dc12c1668dce875c99a45fb49ad5854a13ef4f35.1573638988.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-16T15:11:13Z","receivedAt":"2019-11-16T15:11:21Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the re-roll.\n\n[I removed some parts of the diff to make the reply easier to read. I am \nimplicitly OK with the removed parts.]\n\nOn 13/11/19 09:56AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the revert_helper proc to check for untracked files as well as\n> changes, and then handle changes to be reverted and untracked files with\n> independent blocks of code. Prompt the user independently for untracked\n> files, since the underlying action is fundamentally different (rm -f).\n> If after deleting untracked files, the directory containing them becomes\n> empty, then remove the directory as well. Migrate unlocking of the index\n> out of _close_updateindex to a responsibility of the caller, to permit\n> paths that don't directly unlock the index, and refactor the error\n> handling added in d4e890e5 so that callers can make flow control\n> decisions in the event of errors.\n> \n> A new proc delete_files takes care of actually deleting the files in\n> batches, using the Tcler's Wiki recommended approach for keeping the UI\n> responsive.\n> \n> Since the checkout_index and delete_files calls are both asynchronous\n> and could potentially complete in any order, a \"chord\" is used to\n> coordinate unlocking the index and returning the UI to a usable state\n> only after both operations are complete. The `SimpleChord` class,\n> based on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nLooks much better!\n \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  git-gui.sh    |   4 +-\n>  lib/chord.tcl | 160 +++++++++++++++++++\n>  lib/index.tcl | 416 ++++++++++++++++++++++++++++++++++++++++----------\n>  3 files changed, 496 insertions(+), 84 deletions(-)\n>  create mode 100644 lib/chord.tcl\n> \n> diff --git a/lib/index.tcl b/lib/index.tcl\n> index 28d4d2a54e..3ac08281c2 100644\n> --- a/lib/index.tcl\n> +++ b/lib/index.tcl\n> @@ -7,53 +7,62 @@ proc _delete_indexlock {} {\n>  \t}\n>  }\n>  \n> -proc _close_updateindex {fd after} {\n> -\tglobal use_ttk NS\n> +# Returns true if the operation succeeded, false if a rescan has been initiated.\n> +proc _close_updateindex_rescan_on_error {fd} {\n> +\tif {![catch {_close_updateindex $fd} err]} {\n> +\t\treturn true\n> +\t} else {\n> +\t\trescan_on_error $err\n> +\t\treturn false\n> +\t}\n> +}\n> +\n> +proc _close_updateindex {fd} {\n>  \tfconfigure $fd -blocking 1\n> -\tif {[catch {close $fd} err]} {\n> -\t\tset w .indexfried\n> -\t\tDialog $w\n> -\t\twm withdraw $w\n> -\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n> -\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n> -\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n> -\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n> -\t\t\t-width [string length $s] -relief flat \\\n> -\t\t\t-borderwidth 0 -highlightthickness 0 \\\n> -\t\t\t-background [get_bg_color $w]\n> -\t\t$w.msg tag configure bold -font font_uibold -justify center\n> -\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n> -\t\t$w.msg insert end $s bold \\n\\n$err {}\n> -\t\t$w.msg configure -state disabled\n> -\n> -\t\t${NS}::button $w.continue \\\n> -\t\t\t-text [mc \"Continue\"] \\\n> -\t\t\t-command [list destroy $w]\n> -\t\t${NS}::button $w.unlock \\\n> -\t\t\t-text [mc \"Unlock Index\"] \\\n> -\t\t\t-command \"destroy $w; _delete_indexlock\"\n> -\t\tgrid $w.msg - $w.vs -sticky news\n> -\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n> -\t\tgrid columnconfigure $w 0 -weight 1\n> -\t\tgrid rowconfigure $w 0 -weight 1\n> -\n> -\t\twm protocol $w WM_DELETE_WINDOW update\n> -\t\tbind $w.continue <Visibility> \"\n> -\t\t\tgrab $w\n> -\t\t\tfocus %W\n> -\t\t\"\n> -\t\twm deiconify $w\n> -\t\ttkwait window $w\n> +\tclose $fd\n> +\t$::main_status stop\n\nI didn't spot this earlier. Will this call to 'stop' interfere with the \n'start' in 'delete_files'?\n\n> +}\n>  \n> -\t\t$::main_status stop\n> -\t\tunlock_index\n> -\t\trescan $after 0\n> -\t\treturn\n> -\t}\n> +proc rescan_on_error {err} {\n> +\tglobal use_ttk NS\n> +\n> +\tset w .indexfried\n> +\tDialog $w\n> +\twm withdraw $w\n> +\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n> +\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n> +\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n> +\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n> +\t\t-width [string length $s] -relief flat \\\n> +\t\t-borderwidth 0 -highlightthickness 0 \\\n> +\t\t-background [get_bg_color $w]\n> +\t$w.msg tag configure bold -font font_uibold -justify center\n> +\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n> +\t$w.msg insert end $s bold \\n\\n$err {}\n> +\t$w.msg configure -state disabled\n> +\n> +\t${NS}::button $w.continue \\\n> +\t\t-text [mc \"Continue\"] \\\n> +\t\t-command [list destroy $w]\n> +\t${NS}::button $w.unlock \\\n> +\t\t-text [mc \"Unlock Index\"] \\\n> +\t\t-command \"destroy $w; _delete_indexlock\"\n> +\tgrid $w.msg - $w.vs -sticky news\n> +\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n> +\tgrid columnconfigure $w 0 -weight 1\n> +\tgrid rowconfigure $w 0 -weight 1\n> +\n> +\twm protocol $w WM_DELETE_WINDOW update\n> +\tbind $w.continue <Visibility> \"\n> +\t\tgrab $w\n> +\t\tfocus %W\n> +\t\"\n> +\twm deiconify $w\n> +\ttkwait window $w\n>  \n>  \t$::main_status stop\n\nSame question here.\n\n>  \tunlock_index\n> -\tuplevel #0 $after\n> +\trescan ui_ready 0\n>  }\n>  \n>  proc update_indexinfo {msg path_list after} {\n> @@ -90,7 +99,11 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n> +\t\t\tunlock_index\n> +\t\t}\n> +\n> +\t\tuplevel #0 $after\n\nThis changes when $after is called. If you pass it to 'rescan', it runs \n_after_ the rescan is finished. Now it runs \"in parallel\" with it. Are \nyou sure that is the intended behaviour? Should we just stick to passing \n$after to rescan on failure?\n\n>  \t\treturn\n>  \t}\n>  \n> @@ -156,7 +169,11 @@ proc write_update_index {fd path_list total_cnt batch after} {\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n> +\t\t\tunlock_index\n> +\t\t}\n> +\n> +\t\tuplevel #0 $after\n\nWhile we're here, how about just moving this entire thing to \n'_close_updateindex_rescan_on_error', since the only two consumers of \nthe function do the _exact_ same thing?\n\nThis would also allow us to pass $after to 'rescan'. It would also \nhopefully make the code a bit easier to follow because you can clearly \nsee that we only unlock the index when there is no error.\n\nEven better, unlock the index unconditionally in \n'_close_updateindex_rescan_on_error', and remove the 'unlock_index' call \nfrom 'rescan_on_error'. I generally prefer to keep locking/unlocking \npaths as simple as possible.\n\n>  \t\treturn\n>  \t}\n>  \n> @@ -193,7 +210,7 @@ proc write_update_index {fd path_list total_cnt batch after} {\n>  \t$::main_status update $update_index_cp $total_cnt\n>  }\n>  \n> -proc checkout_index {msg path_list after} {\n> +proc checkout_index {msg path_list after capture_error} {\n>  \tglobal update_index_cp\n>  \n>  \tif {![lock_index update]} return\n> @@ -225,15 +242,21 @@ proc checkout_index {msg path_list after} {\n>  \t\t$total_cnt \\\n>  \t\t$batch \\\n>  \t\t$after \\\n> +\t\t$capture_error \\\n>  \t\t]\n>  }\n>  \n> -proc write_checkout_index {fd path_list total_cnt batch after} {\n> +proc write_checkout_index {fd path_list total_cnt batch after capture_error} {\n>  \tglobal update_index_cp\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\tif {[catch {_close_updateindex $fd} err]} {\n> +\t\t\tuplevel #0 $capture_error [list $err]\n> +\t\t}\n> +\n> +\t\tuplevel #0 $after\n> +\n\nNitpick: Please explicitly mention why we _don't_ want to unlock the \nindex here.\n\nThere are two function very similar to this one: 'write_update_index' \nand 'write_update_indexinfo'. This subtle but important difference is \nvery easy to gloss over.\n\n>  \t\treturn\n>  \t}\n>  \n\nThis patch is almost ready to be merged. Looking forward to the \n(hopefully) final iteration of this topic :)\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"386380","messageId":"CAPSOpYuv1d_yAgBnVBDXMwaA3B3QpmbVKtiSpexpJDwC6Bz33w@mail.gmail.com","threadId":"52154","inReplyTo":"20191116151113.mwbaendh6lgykfw3@yadavpratyush.com","subject":"Re: [PATCH v3 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-16T21:42:28Z","receivedAt":"2019-11-16T21:42:44Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sat, Nov 16, 2019 at 9:11 AM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> > -             grid $w.msg - $w.vs -sticky news\n> > -             grid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n> > -             grid columnconfigure $w 0 -weight 1\n> > -             grid rowconfigure $w 0 -weight 1\n> > -\n> > -             wm protocol $w WM_DELETE_WINDOW update\n> > -             bind $w.continue <Visibility> \"\n> > -                     grab $w\n> > -                     focus %W\n> > -             \"\n> > -             wm deiconify $w\n> > -             tkwait window $w\n> > +     close $fd\n> > +     $::main_status stop\n>\n> I didn't spot this earlier. Will this call to 'stop' interfere with the\n> 'start' in 'delete_files'?\n\nHmm, I think this actually highlights a larger issue. Both\n`write_checkout_index` and `delete_helper` display their progress in\nthe status bar, so if the user elects to do a check-out, and then\nwhile it is still in progress asynchronously, elects to delete files,\nthey'll fight over who gets to set the status. If I'm understanding\ncorrectly, this won't actually interfere with correct operation, but\nof course it won't look very nice.\n\nIf they overlap in this manner, _then_ multiple calls to `stop` could\nbe made, though it does appear that `stop` is idempotent. The Tk\ndocumentation states that `destroy` doesn't return any error if you\npoint it at a window that doesn't exist.\n\n`start` is explicitly idempotent, only creating a new canvas if it\ndoesn't already have one.\n\nI'll see what I can come up with for letting operations more cleanly\nshare the status bar.\n\n> >       if {$update_index_cp >= $total_cnt} {\n> > -             _close_updateindex $fd $after\n> > +             if {[_close_updateindex_rescan_on_error $fd]} {\n> > +                     unlock_index\n> > +             }\n> > +\n> > +             uplevel #0 $after\n>\n> This changes when $after is called. If you pass it to 'rescan', it runs\n> _after_ the rescan is finished. Now it runs \"in parallel\" with it. Are\n> you sure that is the intended behaviour? Should we just stick to passing\n> $after to rescan on failure?\n>\n> [..]\n>\n> While we're here, how about just moving this entire thing to\n> '_close_updateindex_rescan_on_error', since the only two consumers of\n> the function do the _exact_ same thing?\n>\n> This would also allow us to pass $after to 'rescan'. It would also\n> hopefully make the code a bit easier to follow because you can clearly\n> see that we only unlock the index when there is no error.\n>\n> Even better, unlock the index unconditionally in\n> '_close_updateindex_rescan_on_error', and remove the 'unlock_index' call\n> from 'rescan_on_error'. I generally prefer to keep locking/unlocking\n> paths as simple as possible.\n\nHmm, yeah, this makes sense. Pass it `$after`, and then if it calls\n`rescan`, it can hand it off, and `rescan` also (I'm assuming?)\nimplicitly unlocks the index. If it doesn't need to call `rescan`,\nthen `_close_updateindex_rescan_on_error` itself unlocks the index\n_and_ invokes `$after`.\n\n> >       if {$update_index_cp >= $total_cnt} {\n> > -             _close_updateindex $fd $after\n> > +             if {[catch {_close_updateindex $fd} err]} {\n> > +                     uplevel #0 $capture_error [list $err]\n> > +             }\n> > +\n> > +             uplevel #0 $after\n> > +\n>\n> Nitpick: Please explicitly mention why we _don't_ want to unlock the\n> index here.\n>\n> There are two function very similar to this one: 'write_update_index'\n> and 'write_update_indexinfo'. This subtle but important difference is\n> very easy to gloss over.\n\nHmm, so, this suggests a rename of\n`_close_updateindex_rescan_on_error`, because (with the previous\nproposal) it implicitly includes unlocking the index, whereas\n`_close_updateindex` does not.\n\nThanks,\n\nJonathan Gilbert\n"},{"id":"386385","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1573973770.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v4.git.1573973770.gitgitgadget@gmail.com","subject":"[PATCH v4 1/2] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-17T06:56:09Z","receivedAt":"2019-11-17T06:56:17Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"386386","messageId":"pull.436.v4.git.1573973770.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v3.git.1573638988.gitgitgadget@gmail.com","subject":"[PATCH v4 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-17T06:56:08Z","receivedAt":"2019-11-17T06:56:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nSince the checkout_index and delete_files calls are both asynchronous and\noverlap, they clash in wanting to update the status bar. To address this,\nthe status bar is reworked so that when an operation wants to display\nongoing updates/progress, it explicitly starts an \"operation\", which is\ntracked by its own object, and the status bar handles multiple concurrent\noperations by merging their progress and concatenating their text.\n\nThe _close_updateindex proc contains error handling (added in d4e890e5) that\nhas the potential to interact badly with unlock_index running at the\ncompletion of an async operation. I have refactored the procedure into\nseparate procs _close_updateindex and rescan_on_error, and the original\nfunctionality is captured with _close_updateindex_rescan_on_error. Call\nsites have been updated appropriately, and checkout_index has been reworked\nto take a functor that captures any errors that occur in a caller-defined\nway. revert_helper uses this to supply a lambda function that stashes the\nerror within the chord's body namespace, so that it can call rescan_on_error \nwhen the chord is completed, which might be substantially after \ncheckout_index encounters its error. If a rescan is called for, it is done\nonce the deletion is complete.\n\nThis is the fifth revision of this change, which differs from the fourth\nversion in the following ways:\n\n * The status_bar.tcl code has been reworked so that tracking an operation\n   with updates and progress is now done in instances of a separate \n   status_bar_operation class. main_status start returns an operation\n   instead of directly setting the state on the main progress bar, and then\n   you send all your updates to the operation. This model allows the main\n   progress bar to support multiple ongoing operations, in which case their\n   progress data is merged: The status text is concatenated (separated by\n   '/'), and the progress is combined mathematically into a single progress\n   bar.\n   \n   \n * All call sites that used to directly send updates to main_status have\n   been updated to use the new model.\n   \n   \n * The initialization code in git-gui.sh (which I'm assuming translates\n   somehow to git-gui.tcl in the installation?) now explicitly clears the\n   \"Initializing...\" status bar text, since the new status bar model won't\n   do this automatically when operations are performed.\n   \n   \n * rescan_on_error makes a call to a special status bar function to stop and\n   unhook all ongoing operations, on the assumption that the rescan is\n   superseding any and all ongoing operations. Of course, if the operations\n   are still actually running, errors will result, but this seemed the\n   clearest mapping from the previous revision's $::main_status stop call\n   inside the rescan code.\n   \n   \n * _close_updateindex_rescan_on_error has been renamed to \n   close_and_unlock_updateindex_rescan_on_error, and once again takes $after\n   . As indicated by the name change, it also encapsulates the unlocking of\n   the index, so that is removed from the call sites.\n   \n   \n * rescan_on_error takes $after as well, in case \n   close_and_unlock_updateindex_rescan_on_error needs to call it.\n   \n   \n * The after_chord body in revert_helper only calls unlock_index if it\n   doesn't call rescan_on_error.\n   \n   \n\ngit remote add logiclrd https://github.com/logiclrd/git.git\ngit fetch logiclrd git-gui-revert-untracked revision4\ngit diff dc12c16..23d4f5d\n\nJonathan Gilbert (2):\n  git-gui: consolidate naming conventions\n  git-gui: revert untracked files by deleting them\n\n git-gui.sh          |   7 +-\n lib/checkout_op.tcl |  15 +-\n lib/chord.tcl       | 160 ++++++++++++++\n lib/index.tcl       | 517 +++++++++++++++++++++++++++++++++-----------\n lib/merge.tcl       |  14 +-\n lib/status_bar.tcl  | 221 +++++++++++++++++--\n 6 files changed, 771 insertions(+), 163 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v3:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n 2:  dc12c1668d ! 2:  23d4f5d829 git-gui: revert untracked files by deleting them\n     @@ -11,7 +11,8 @@\n          out of _close_updateindex to a responsibility of the caller, to permit\n          paths that don't directly unlock the index, and refactor the error\n          handling added in d4e890e5 so that callers can make flow control\n     -    decisions in the event of errors.\n     +    decisions in the event of errors. Rework status_bar to explicitly handle\n     +    multiple overlapping operations, and update all call sites.\n      \n          A new proc delete_files takes care of actually deleting the files in\n          batches, using the Tcler's Wiki recommended approach for keeping the UI\n     @@ -23,6 +24,14 @@\n          only after both operations are complete. The `SimpleChord` class,\n          based on TclOO (Tcl/Tk 8.6), is added in this commit.\n      \n     +    Since the checkout_index and delete_files calls are both asynchronous\n     +    and overlap, they clash in wanting to update the status bar. This commit\n     +    reworks the status bar so that when an operation wants to display\n     +    ongoing updates/progress, it explicitly starts an \"operation\", which is\n     +    tracked by its own object, and the status bar handles multiple\n     +    concurrent operations by merging their progress and concatenating their\n     +    text.\n     +\n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n       diff --git a/git-gui.sh b/git-gui.sh\n     @@ -39,6 +48,73 @@\n       } {\n       \tcatch {wm withdraw .}\n       \ttk_messageBox \\\n     +@@\n     + \tdo_explore\n     + }\n     + \n     ++# Clear \"Initializing...\" status\n     ++after idle {after 500 {$main_status show \"\"}}\n     ++\n     + # Local variables:\n     + # mode: tcl\n     + # indent-tabs-mode: t\n     +\n     + diff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\n     + --- a/lib/checkout_op.tcl\n     + +++ b/lib/checkout_op.tcl\n     +@@\n     + \tglobal HEAD\n     + \n     + \tset readtree_d {}\n     +-\t$::main_status start \\\n     ++\tset status_bar_operation [$::main_status start \\\n     + \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n     +-\t\t[mc \"files checked out\"]\n     ++\t\t[mc \"files checked out\"]]\n     + \n     + \tset fd [git_read --stderr read-tree \\\n     + \t\t-m \\\n     +@@\n     + \t\t$new_hash \\\n     + \t\t]\n     + \tfconfigure $fd -blocking 0 -translation binary\n     +-\tfileevent $fd readable [cb _readtree_wait $fd]\n     ++\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n     + }\n     + \n     +-method _readtree_wait {fd} {\n     ++method _readtree_wait {fd status_bar_operation} {\n     + \tglobal current_branch\n     + \n     + \tset buf [read $fd]\n     +-\t$::main_status update_meter $buf\n     ++\t$status_bar_operation update_meter $buf\n     + \tappend readtree_d $buf\n     + \n     + \tfconfigure $fd -blocking 1\n     + \tif {![eof $fd]} {\n     + \t\tfconfigure $fd -blocking 0\n     ++\t\t$status_bar_operation stop\n     + \t\treturn\n     + \t}\n     + \n     + \tif {[catch {close $fd}]} {\n     + \t\tset err $readtree_d\n     + \t\tregsub {^fatal: } $err {} err\n     +-\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n     ++\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n     + \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n     + \n     + $err\n     +@@\n     + \t\treturn\n     + \t}\n     + \n     +-\t$::main_status stop\n     ++\t$status_bar_operation stop\n     + \t_after_readtree $this\n     + }\n     + \n      \n       diff --git a/lib/chord.tcl b/lib/chord.tcl\n       new file mode 100644\n     @@ -215,18 +291,7 @@\n       \n      -proc _close_updateindex {fd after} {\n      -\tglobal use_ttk NS\n     -+# Returns true if the operation succeeded, false if a rescan has been initiated.\n     -+proc _close_updateindex_rescan_on_error {fd} {\n     -+\tif {![catch {_close_updateindex $fd} err]} {\n     -+\t\treturn true\n     -+\t} else {\n     -+\t\trescan_on_error $err\n     -+\t\treturn false\n     -+\t}\n     -+}\n     -+\n     -+proc _close_updateindex {fd} {\n     - \tfconfigure $fd -blocking 1\n     +-\tfconfigure $fd -blocking 1\n      -\tif {[catch {close $fd} err]} {\n      -\t\tset w .indexfried\n      -\t\tDialog $w\n     @@ -261,16 +326,29 @@\n      -\t\t\"\n      -\t\twm deiconify $w\n      -\t\ttkwait window $w\n     -+\tclose $fd\n     -+\t$::main_status stop\n     -+}\n     - \n     +-\n      -\t\t$::main_status stop\n     --\t\tunlock_index\n     ++# Returns true if the operation succeeded, false if a rescan has been initiated.\n     ++proc close_and_unlock_updateindex_rescan_on_error {fd after} {\n     ++\tif {![catch {_close_updateindex $fd} err]} {\n     + \t\tunlock_index\n      -\t\trescan $after 0\n      -\t\treturn\n     --\t}\n     -+proc rescan_on_error {err} {\n     ++\t\tuplevel #0 $after\n     ++\t\treturn 1\n     ++\t} else {\n     ++\t\trescan_on_error $err $after\n     ++\t\treturn 0\n     + \t}\n     ++}\n     + \n     +-\t$::main_status stop\n     ++proc _close_updateindex {fd} {\n     ++\tfconfigure $fd -blocking 1\n     ++\tclose $fd\n     ++}\n     ++\n     ++proc rescan_on_error {err {after {}}} {\n      +\tglobal use_ttk NS\n      +\n      +\tset w .indexfried\n     @@ -306,42 +384,91 @@\n      +\t\"\n      +\twm deiconify $w\n      +\ttkwait window $w\n     - \n     - \t$::main_status stop\n     ++\n     ++\t$::main_status stop_all\n       \tunlock_index\n      -\tuplevel #0 $after\n     -+\trescan ui_ready 0\n     ++\trescan [concat $after [list ui_ready]] 0\n       }\n       \n       proc update_indexinfo {msg path_list after} {\n      @@\n     + \tset batch [expr {int($total_cnt * .01) + 1}]\n     + \tif {$batch > 25} {set batch 25}\n     + \n     +-\t$::main_status start $msg [mc \"files\"]\n     ++\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     + \tset fd [git_write update-index -z --index-info]\n     + \tfconfigure $fd \\\n     + \t\t-blocking 0 \\\n     +@@\n     + \t\t$path_list \\\n     + \t\t$total_cnt \\\n     + \t\t$batch \\\n     ++\t\t$status_bar_operation \\\n     + \t\t$after \\\n     + \t\t]\n     + }\n     + \n     +-proc write_update_indexinfo {fd path_list total_cnt batch after} {\n     ++proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n     ++\tafter} {\n     + \tglobal update_index_cp\n       \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     -+\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n     -+\t\t\tunlock_index\n     -+\t\t}\n     -+\n     -+\t\tuplevel #0 $after\n     ++\t\t$status_bar_operation stop\n     ++\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n       \t\treturn\n       \t}\n       \n      @@\n     + \t\tdisplay_file $path $new\n     + \t}\n     + \n     +-\t$::main_status update $update_index_cp $total_cnt\n     ++\t$status_bar_operation update $update_index_cp $total_cnt\n     + }\n     + \n     + proc update_index {msg path_list after} {\n     +@@\n     + \tset batch [expr {int($total_cnt * .01) + 1}]\n     + \tif {$batch > 25} {set batch 25}\n     + \n     +-\t$::main_status start $msg [mc \"files\"]\n     ++\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     + \tset fd [git_write update-index --add --remove -z --stdin]\n     + \tfconfigure $fd \\\n     + \t\t-blocking 0 \\\n     +@@\n     + \t\t$path_list \\\n     + \t\t$total_cnt \\\n     + \t\t$batch \\\n     ++\t\t$status_bar_operation \\\n     + \t\t$after \\\n     + \t\t]\n     + }\n     + \n     +-proc write_update_index {fd path_list total_cnt batch after} {\n     ++proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n     ++\tafter} {\n     + \tglobal update_index_cp\n       \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     -+\t\tif {[_close_updateindex_rescan_on_error $fd]} {\n     -+\t\t\tunlock_index\n     -+\t\t}\n     -+\n     -+\t\tuplevel #0 $after\n     ++\t\t$status_bar_operation stop\n     ++\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n       \t\treturn\n       \t}\n       \n      @@\n     - \t$::main_status update $update_index_cp $total_cnt\n     + \t\tdisplay_file $path $new\n     + \t}\n     + \n     +-\t$::main_status update $update_index_cp $total_cnt\n     ++\t$status_bar_operation update $update_index_cp $total_cnt\n       }\n       \n      -proc checkout_index {msg path_list after} {\n     @@ -350,20 +477,34 @@\n       \n       \tif {![lock_index update]} return\n      @@\n     + \tset batch [expr {int($total_cnt * .01) + 1}]\n     + \tif {$batch > 25} {set batch 25}\n     + \n     +-\t$::main_status start $msg [mc \"files\"]\n     ++\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     + \tset fd [git_write checkout-index \\\n     + \t\t--index \\\n     + \t\t--quiet \\\n     +@@\n     + \t\t$path_list \\\n       \t\t$total_cnt \\\n       \t\t$batch \\\n     ++\t\t$status_bar_operation \\\n       \t\t$after \\\n      +\t\t$capture_error \\\n       \t\t]\n       }\n       \n      -proc write_checkout_index {fd path_list total_cnt batch after} {\n     -+proc write_checkout_index {fd path_list total_cnt batch after capture_error} {\n     ++proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n     ++\tafter capture_error} {\n       \tglobal update_index_cp\n       \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n      -\t\t_close_updateindex $fd $after\n     ++\t\t$status_bar_operation stop\n     ++\n      +\t\tif {[catch {_close_updateindex $fd} err]} {\n      +\t\t\tuplevel #0 $capture_error [list $err]\n      +\t\t}\n     @@ -373,6 +514,15 @@\n       \t\treturn\n       \t}\n       \n     +@@\n     + \t\t}\n     + \t}\n     + \n     +-\t$::main_status update $update_index_cp $total_cnt\n     ++\t$status_bar_operation update $update_index_cp $total_cnt\n     + }\n     + \n     + proc unstage_helper {txt paths} {\n      @@\n       \tadd_helper [mc \"Adding all changed files\"] $paths\n       }\n     @@ -394,10 +544,10 @@\n      +\t# The asynchronous operations are each indicated below by a comment\n      +\t# before the code block that starts the async operation.\n      +\tset after_chord [SimpleChord new {\n     -+\t\tunlock_index\n      +\t\tif {[string trim $err] != \"\"} {\n      +\t\t\trescan_on_error $err\n      +\t\t} else {\n     ++\t\t\tunlock_index\n      +\t\t\tif {$should_reshow_diff} { reshow_diff }\n      +\t\t\tui_ready\n      +\t\t}\n     @@ -467,11 +617,6 @@\n      +\t\t\t\t\"Revert changes in these %i files?\" \\\n      +\t\t\t\t$path_cnt]\n      +\t\t}\n     -+\n     -+\t\tset reply [tk_dialog \\\n     -+\t\t\t.confirm_revert \\\n     -+\t\t\t\"[appname] ([reponame])\" \\\n     -+\t\t\t\"$query\n       \n      -\t# Split question between singular and plural cases, because\n      -\t# such distinction is needed in some languages. Previously, the\n     @@ -491,6 +636,11 @@\n      -\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n      -\t} else {\n      -\t\tset query [mc \"Revert changes in these %i files?\" $n]\n     ++\t\tset reply [tk_dialog \\\n     ++\t\t\t.confirm_revert \\\n     ++\t\t\t\"[appname] ([reponame])\" \\\n     ++\t\t\t\"$query\n     ++\n      +[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n      +\t\t\tquestion \\\n      +\t\t\t1 \\\n     @@ -571,7 +721,10 @@\n      +# UI to remain responsive and updated.\n      +proc delete_files {path_list after} {\n      +\t# Enable progress bar status updates\n     -+\t$::main_status start [mc \"Deleting\"] [mc \"files\"]\n     ++\tset status_bar_operation [$::main_status \\\n     ++\t\tstart \\\n     ++\t\t[mc \"Deleting\"] \\\n     ++\t\t[mc \"files\"]]\n      +\n      +\tset path_index 0\n      +\tset deletion_errors [list]\n     @@ -582,13 +735,15 @@\n      +\t\t$path_index \\\n      +\t\t$deletion_errors \\\n      +\t\t$batch_size \\\n     ++\t\t$status_bar_operation \\\n      +\t\t$after\n      +}\n      +\n      +# Helper function to delete a list of files in batches. Each call deletes one\n      +# batch of files, and then schedules a call for the next batch after any UI\n      +# messages have been processed.\n     -+proc delete_helper {path_list path_index deletion_errors batch_size after} {\n     ++proc delete_helper {path_list path_index deletion_errors batch_size \\\n     ++\tstatus_bar_operation after} {\n      +\tglobal file_states\n      +\n      +\tset path_cnt [llength $path_list]\n     @@ -622,7 +777,7 @@\n      +\t# Update the progress bar to indicate that this batch has been\n      +\t# completed. The update will be visible when this procedure returns\n      +\t# and allows the UI thread to process messages.\n     -+\t$::main_status update $path_index $path_cnt\n     ++\t$status_bar_operation update $path_index $path_cnt\n      +\n      +\tif {$path_index < $path_cnt} {\n      +\t\t# The Tcler's Wiki lists this as the best practice for keeping\n     @@ -636,12 +791,13 @@\n      +\t\t\t$path_index \\\n      +\t\t\t$deletion_errors \\\n      +\t\t\t$batch_size \\\n     ++\t\t\t$status_bar_operation \\\n      +\t\t\t$after\n      +\t\t\t]]\n       \t} else {\n      -\t\tunlock_index\n      +\t\t# Finish the status bar operation.\n     -+\t\t$::main_status stop\n     ++\t\t$status_bar_operation stop\n      +\n      +\t\t# Report error, if any, based on how many deletions failed.\n      +\t\tset deletion_error_cnt [llength $deletion_errors]\n     @@ -704,3 +860,348 @@\n       \t}\n       }\n       \n     +\n     + diff --git a/lib/merge.tcl b/lib/merge.tcl\n     + --- a/lib/merge.tcl\n     + +++ b/lib/merge.tcl\n     +@@\n     + \tif {[ask_popup $op_question] eq {yes}} {\n     + \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n     + \t\tfconfigure $fd -blocking 0 -translation binary\n     +-\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n     +-\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n     ++\t\tset status_bar_operation [$::main_status \\\n     ++\t\t\tstart \\\n     ++\t\t\t[mc \"Aborting\"] \\\n     ++\t\t\t[mc \"files reset\"]\n     ++\t\tfileevent $fd readable [namespace code [list \\\n     ++\t\t\t_reset_wait $fd $status_bar_operation]]\n     + \t} else {\n     + \t\tunlock_index\n     + \t}\n     + }\n     + \n     +-proc _reset_wait {fd} {\n     ++proc _reset_wait {fd status_bar_operation} {\n     + \tglobal ui_comm\n     + \n     +-\t$::main_status update_meter [read $fd]\n     ++\t$status_bar_operation update_meter [read $fd]\n     + \n     + \tfconfigure $fd -blocking 1\n     + \tif {[eof $fd]} {\n     + \t\tset fail [catch {close $fd} err]\n     +-\t\t$::main_status stop\n     + \t\tunlock_index\n     ++\t\t$status_bar_operation stop\n     + \n     + \t\t$ui_comm delete 0.0 end\n     + \t\t$ui_comm edit modified false\n     +\n     + diff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\n     + --- a/lib/status_bar.tcl\n     + +++ b/lib/status_bar.tcl\n     +@@\n     + # git-gui status bar mega-widget\n     + # Copyright (C) 2007 Shawn Pearce\n     + \n     ++# The status_bar class manages the entire status bar. It is possible for\n     ++# multiple overlapping asynchronous operations to want to display status\n     ++# simultaneously. Each one receives a status_bar_operation when it calls the\n     ++# start method, and the status bar combines all active operations into the\n     ++# line of text it displays. Most of the time, there will be at most one\n     ++# ongoing operation.\n     ++#\n     ++# Note that the entire status bar can be either in single-line or two-line\n     ++# mode, depending on the constructor. Multiple active operations are only\n     ++# supported for single-line status bars.\n     ++\n     + class status_bar {\n     + \n     ++field allow_multiple ; # configured at construction\n     ++\n     + field w         ; # our own window path\n     + field w_l       ; # text widget we draw messages into\n     + field w_c       ; # canvas we draw a progress bar into\n     + field c_pack    ; # script to pack the canvas with\n     +-field status  {}; # single line of text we show\n     +-field prefix  {}; # text we format into status\n     +-field units   {}; # unit of progress\n     +-field meter   {}; # current core git progress meter (if active)\n     ++\n     ++field baseline_text   ; # text to show if there are no operations\n     ++field status_bar_text ; # combined text for all operations\n     ++\n     ++field operations ; # list of current ongoing operations\n     ++\n     ++# The status bar can display a progress bar, updated when consumers call the\n     ++# update method on their status_bar_operation. When there are multiple\n     ++# operations, the status bar shows the combined status of all operations.\n     ++#\n     ++# When an overlapping operation completes, the progress bar is going to\n     ++# abruptly have one fewer operation in the calculation, causing a discontinuity.\n     ++# Therefore, whenever an operation completes, if it is not the last operation,\n     ++# this counter is increased, and the progress bar is calculated as though there\n     ++# were still another operation at 100%. When the last operation completes, this\n     ++# is reset to 0.\n     ++field completed_operation_count\n     + \n     + constructor new {path} {\n     + \tglobal use_ttk NS\n     +@@\n     + \tset w_l $w.l\n     + \tset w_c $w.c\n     + \n     ++\t# Standard single-line status bar: Permit overlapping operations\n     ++\tset allow_multiple 1\n     ++\n     ++\tset baseline_text \"\"\n     ++\tset operations [list]\n     ++\tset completed_operation_count 0\n     ++\n     + \t${NS}::frame $w\n     + \tif {!$use_ttk} {\n     + \t\t$w configure -borderwidth 1 -relief sunken\n     + \t}\n     + \t${NS}::label $w_l \\\n     +-\t\t-textvariable @status \\\n     ++\t\t-textvariable @status_bar_text \\\n     + \t\t-anchor w \\\n     + \t\t-justify left\n     + \tpack $w_l -side left\n     +@@\n     + \tset w_l $w.l\n     + \tset w_c $w.c\n     + \n     ++\t# Two-line status bar: Only one ongoing operation permitted.\n     ++\tset allow_multiple 0\n     ++\n     ++\tset baseline_text \"\"\n     ++\tset operations [list]\n     ++\n     + \t${NS}::frame $w\n     + \t${NS}::label $w_l \\\n     +-\t\t-textvariable @status \\\n     ++\t\t-textvariable @status_bar_text \\\n     + \t\t-anchor w \\\n     + \t\t-justify left\n     + \tpack $w_l -anchor w -fill x\n     +@@\n     + \treturn $this\n     + }\n     + \n     +-method start {msg uds} {\n     ++method ensure_canvas {} {\n     + \tif {[winfo exists $w_c]} {\n     + \t\t$w_c coords bar 0 0 0 20\n     + \t} else {\n     +@@\n     + \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n     + \t\teval $c_pack\n     + \t}\n     ++}\n     ++\n     ++method show {msg {test {}}} {\n     ++\tif {$test eq {} || $status eq $test} {\n     ++\t\t$this ensure_canvas\n     ++\t\tset baseline_text $msg\n     ++\t\t$this refresh\n     ++\t}\n     ++}\n     ++\n     ++method start {msg uds} {\n     ++\tset baseline_text \"\"\n     ++\n     ++\tif {!$allow_multiple && [llength $operations]} {\n     ++\t\treturn [lindex $operations 0]\n     ++\t}\n     ++\n     ++\t$this ensure_canvas\n     ++\n     ++\tset operation [status_bar_operation::new $this $msg $uds]\n     ++\n     ++\tlappend operations $operation\n     ++\n     ++\t$this refresh\n     ++\n     ++\treturn $operation\n     ++}\n     ++\n     ++method refresh {} {\n     ++\tset new_text \"\"\n     ++\n     ++\tset total [expr $completed_operation_count * 100]\n     ++\tset have $total\n     ++\n     ++\tforeach operation $operations {\n     ++\t\tif {$new_text != \"\"} {\n     ++\t\t\tappend new_text \" / \"\n     ++\t\t}\n     ++\n     ++\t\tappend new_text [$operation get_status]\n     ++\n     ++\t\tset total [expr $total + 100]\n     ++\t\tset have [expr $have + [$operation get_progress]]\n     ++\t}\n     ++\n     ++\tif {$new_text == \"\"} {\n     ++\t\tset new_text $baseline_text\n     ++\t}\n     ++\n     ++\tset status_bar_text $new_text\n     ++\n     ++\tset pixel_width 0\n     ++\tif {$have > 0} {\n     ++\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n     ++\t}\n     ++\n     ++\t$w_c coords bar 0 0 $pixel_width 20\n     ++}\n     ++\n     ++method stop {operation stop_msg} {\n     ++\tset idx [lsearch $operations $operation]\n     ++\n     ++\tif {$idx >= 0} {\n     ++\t\tset operations [lreplace $operations $idx $idx]\n     ++\t\tset completed_operation_count [expr \\\n     ++\t\t\t$completed_operation_count + 1]\n     ++\n     ++\t\tif {[llength operations] == 0} {\n     ++\t\t\tset completed_operation_count 0\n     ++\n     ++\t\t\tdestroy $w_c\n     ++\t\t\tif {$stop_msg ne {}} {\n     ++\t\t\t\tset baseline_text $stop_msg\n     ++\t\t\t}\n     ++\t\t}\n     ++\n     ++\t\t$this refresh\n     ++\t}\n     ++}\n     ++\n     ++method stop_all {{stop_msg {}}} {\n     ++\tset operations_copy $operations\n     ++\tset operations [list] # This makes the operation's call to stop a no-op.\n     ++\n     ++\tforeach $operation operations_copy {\n     ++\t\t$operation stop\n     ++\t}\n     ++\n     ++\tif {$stop_msg ne {}} {\n     ++\t\tset baseline_text $stop_msg\n     ++\t}\n     ++\n     ++\t$this refresh\n     ++}\n     ++\n     ++method _delete {current} {\n     ++\tif {$current eq $w} {\n     ++\t\tdelete_this\n     ++\t}\n     ++}\n     ++\n     ++}\n     ++\n     ++# The status_bar_operation class tracks a single consumer's ongoing status bar\n     ++# activity, with the context that there are a few situations where multiple\n     ++# overlapping asynchronous operations might want to display status information\n     ++# simultaneously. Instances of status_bar_operation are created by calling\n     ++# start on the status_bar, and when the caller is done with its stauts bar\n     ++# operation, it calls stop on the operation.\n     ++\n     ++class status_bar_operation {\n     ++\n     ++field status_bar; # reference back to the status_bar that owns this object\n     ++\n     ++field is_active;\n     ++\n     ++field status   {}; # single line of text we show\n     ++field progress {}; # current progress (0 to 100)\n     ++field prefix   {}; # text we format into status\n     ++field units    {}; # unit of progress\n     ++field meter    {}; # current core git progress meter (if active)\n     ++\n     ++constructor new {owner msg uds} {\n     ++\tset status_bar $owner\n     + \n     + \tset status $msg\n     ++\tset progress 0\n     + \tset prefix $msg\n     + \tset units  $uds\n     + \tset meter  {}\n     ++\n     ++\tset is_active 1\n     ++\n     ++\treturn $this\n     + }\n     + \n     ++method get_is_active {} { return $is_active }\n     ++method get_status {} { return $status }\n     ++method get_progress {} { return $progress }\n     ++\n     + method update {have total} {\n     +-\tset pdone 0\n     +-\tset cdone 0\n     ++\tif {!$is_active} { return }\n     ++\n     ++\tset progress 0\n     ++\n     + \tif {$total > 0} {\n     +-\t\tset pdone [expr {100 * $have / $total}]\n     +-\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n     ++\t\tset progress [expr {100 * $have / $total}]\n     + \t}\n     + \n     + \tset prec [string length [format %i $total]]\n     ++\n     + \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n     + \t\t$prefix \\\n     + \t\t$prec $have \\\n     + \t\t$prec $total \\\n     +-\t\t$units $pdone]\n     +-\t$w_c coords bar 0 0 $cdone 20\n     ++\t\t$units $progress]\n     ++\n     ++\t$status_bar refresh\n     + }\n     + \n     + method update_meter {buf} {\n     ++\tif {!$is_active} { return }\n     ++\n     + \tappend meter $buf\n     + \tset r [string last \"\\r\" $meter]\n     + \tif {$r == -1} {\n     +@@\n     + \t}\n     + }\n     + \n     +-method stop {{msg {}}} {\n     +-\tdestroy $w_c\n     +-\tif {$msg ne {}} {\n     +-\t\tset status $msg\n     ++method stop {{stop_msg {}}} {\n     ++\tif {$is_active} {\n     ++\t\tset is_active 0\n     ++\t\t$status_bar stop $this $stop_msg\n     + \t}\n     + }\n     + \n     + method show {msg {test {}}} {\n     ++\tif {!$is_active} { return }\n     ++\n     + \tif {$test eq {} || $status eq $test} {\n     + \t\tset status $msg\n     ++\t\t$status_bar refresh\n     + \t}\n     + }\n     + \n     +-method _delete {current} {\n     +-\tif {$current eq $w} {\n     +-\t\tdelete_this\n     +-\t}\n     ++method _delete {} {\n     ++\tstop\n     ++\tdelete_this\n     + }\n     + \n     + }\n\n-- \ngitgitgadget\n"},{"id":"386387","messageId":"23d4f5d829cb705441394186d274620b46bae8fb.1573973770.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v4.git.1573973770.gitgitgadget@gmail.com","subject":"[PATCH v4 2/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-17T06:56:10Z","receivedAt":"2019-11-17T06:56:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper proc to check for untracked files as well as\nchanges, and then handle changes to be reverted and untracked files with\nindependent blocks of code. Prompt the user independently for untracked\nfiles, since the underlying action is fundamentally different (rm -f).\nIf after deleting untracked files, the directory containing them becomes\nempty, then remove the directory as well. Migrate unlocking of the index\nout of _close_updateindex to a responsibility of the caller, to permit\npaths that don't directly unlock the index, and refactor the error\nhandling added in d4e890e5 so that callers can make flow control\ndecisions in the event of errors. Rework status_bar to explicitly handle\nmultiple overlapping operations, and update all call sites.\n\nA new proc delete_files takes care of actually deleting the files in\nbatches, using the Tcler's Wiki recommended approach for keeping the UI\nresponsive.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand could potentially complete in any order, a \"chord\" is used to\ncoordinate unlocking the index and returning the UI to a usable state\nonly after both operations are complete. The `SimpleChord` class,\nbased on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand overlap, they clash in wanting to update the status bar. This commit\nreworks the status bar so that when an operation wants to display\nongoing updates/progress, it explicitly starts an \"operation\", which is\ntracked by its own object, and the status bar handles multiple\nconcurrent operations by merging their progress and concatenating their\ntext.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh          |   7 +-\n lib/checkout_op.tcl |  15 +-\n lib/chord.tcl       | 160 ++++++++++++++++\n lib/index.tcl       | 443 +++++++++++++++++++++++++++++++++++---------\n lib/merge.tcl       |  14 +-\n lib/status_bar.tcl  | 221 +++++++++++++++++++---\n 6 files changed, 734 insertions(+), 126 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex 0d21f5688b..dc4ac577ac 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n ##\n ## Tcl/Tk sanity check\n \n-if {[catch {package require Tcl 8.4} err]\n- || [catch {package require Tk  8.4} err]\n+if {[catch {package require Tcl 8.6} err]\n+ || [catch {package require Tk  8.6} err]\n } {\n \tcatch {wm withdraw .}\n \ttk_messageBox \\\n@@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n \tdo_explore\n }\n \n+# Clear \"Initializing...\" status\n+after idle {after 500 {$main_status show \"\"}}\n+\n # Local variables:\n # mode: tcl\n # indent-tabs-mode: t\ndiff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\nindex a5228297db..21ea768d80 100644\n--- a/lib/checkout_op.tcl\n+++ b/lib/checkout_op.tcl\n@@ -341,9 +341,9 @@ method _readtree {} {\n \tglobal HEAD\n \n \tset readtree_d {}\n-\t$::main_status start \\\n+\tset status_bar_operation [$::main_status start \\\n \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n-\t\t[mc \"files checked out\"]\n+\t\t[mc \"files checked out\"]]\n \n \tset fd [git_read --stderr read-tree \\\n \t\t-m \\\n@@ -354,26 +354,27 @@ method _readtree {} {\n \t\t$new_hash \\\n \t\t]\n \tfconfigure $fd -blocking 0 -translation binary\n-\tfileevent $fd readable [cb _readtree_wait $fd]\n+\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n }\n \n-method _readtree_wait {fd} {\n+method _readtree_wait {fd status_bar_operation} {\n \tglobal current_branch\n \n \tset buf [read $fd]\n-\t$::main_status update_meter $buf\n+\t$status_bar_operation update_meter $buf\n \tappend readtree_d $buf\n \n \tfconfigure $fd -blocking 1\n \tif {![eof $fd]} {\n \t\tfconfigure $fd -blocking 0\n+\t\t$status_bar_operation stop\n \t\treturn\n \t}\n \n \tif {[catch {close $fd}]} {\n \t\tset err $readtree_d\n \t\tregsub {^fatal: } $err {} err\n-\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n+\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n \n $err\n@@ -384,7 +385,7 @@ $err\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$status_bar_operation stop\n \t_after_readtree $this\n }\n \ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..275a6cd4a1\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,160 @@\n+# Simple Chord for Tcl\n+#\n+# A \"chord\" is a method with more than one entrypoint and only one body, such\n+# that the body runs only once all the entrypoints have been called by\n+# different asynchronous tasks. In this implementation, the chord is defined\n+# dynamically for each invocation. A SimpleChord object is created, supplying\n+# body script to be run when the chord is completed, and then one or more notes\n+# are added to the chord. Each note can be called like a proc, and returns\n+# immediately if the chord isn't yet complete. When the last remaining note is\n+# called, the body runs before the note returns.\n+#\n+# The SimpleChord class has a constructor that takes the body script, and a\n+# method add_note that returns a note object. Since the body script does not\n+# run in the context of the procedure that defined it, a mechanism is provided\n+# for injecting variables into the chord for use by the body script. The\n+# activation of a note is idempotent; multiple calls have the same effect as\n+# a simple call.\n+#\n+# If you are invoking asynchronous operations with chord notes as completion\n+# callbacks, and there is a possibility that earlier operations could complete\n+# before later ones are started, it is a good practice to create a \"common\"\n+# note on the chord that prevents it from being complete until you're certain\n+# you've added all the notes you need.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+oo::class create SimpleChord {\n+\tvariable notes body is_completed\n+\n+\t# Constructor:\n+\t#   set chord [SimpleChord new {body}]\n+\t#     Creates a new chord object with the specified body script. The\n+\t#     body script is evaluated at most once, when a note is activated\n+\t#     and the chord has no other non-activated notes.\n+\tconstructor {body} {\n+\t\tset notes [list]\n+\t\tmy eval [list set body $body]\n+\t\tset is_completed 0\n+\t}\n+\n+\t# Method:\n+\t#   $chord eval {script}\n+\t#     Runs the specified script in the same context (namespace) in which\n+\t#     the chord body will be evaluated. This can be used to set variable\n+\t#     values for the chord body to use.\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\t# Method:\n+\t#   set note [$chord add_note]\n+\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n+\t#     error if the chord is already completed, otherwise the chord is\n+\t#     updated so that the new note must also be activated before the\n+\t#     body is evaluated.\n+\tmethod add_note {} {\n+\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\t# This method is for internal use only and is intentionally undocumented.\n+\tmethod notify_note_activation {} {\n+\t\tif {!$is_completed} {\n+\t\t\tforeach note $notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset is_completed 1\n+\n+\t\t\tnamespace eval [self] $body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+oo::class create ChordNote {\n+\tvariable chord is_activated\n+\n+\t# Constructor:\n+\t#   Instances of ChordNote are created internally by calling add_note on\n+\t#   SimpleChord objects.\n+\tconstructor {chord} {\n+\t\tmy eval set chord $chord\n+\t\tset is_activated 0\n+\t}\n+\n+\t# Method:\n+\t#   [$note is_activated]\n+\t#     Returns true if this note has already been activated.\n+\tmethod is_activated {} {\n+\t\treturn $is_activated\n+\t}\n+\n+\t# Method:\n+\t#   $note\n+\t#     Activates the note, if it has not already been activated, and\n+\t#     completes the chord if there are no other notes awaiting\n+\t#     activation. Subsequent calls will have no further effect.\n+\t#\n+\t# NB: In TclOO, if an object is invoked like a method without supplying\n+\t#     any method name, then this internal method `unknown` is what\n+\t#     actually runs (with no parameters). It is used in the ChordNote\n+\t#     class for the purpose of allowing the note object to be called as\n+\t#     a function (see example above). (The `unknown` method can also be\n+\t#     used to support dynamic dispatch, but must take parameters to\n+\t#     identify the \"unknown\" method to be invoked. In this form, this\n+\t#     proc serves only to make instances behave directly like methods.)\n+\tmethod unknown {} {\n+\t\tif {!$is_activated} {\n+\t\t\tset is_activated 1\n+\t\t\t$chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..8d7590241e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,53 +7,63 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n-\tglobal use_ttk NS\n-\tfconfigure $fd -blocking 1\n-\tif {[catch {close $fd} err]} {\n-\t\tset w .indexfried\n-\t\tDialog $w\n-\t\twm withdraw $w\n-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n-\t\t\t-width [string length $s] -relief flat \\\n-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n-\t\t\t-background [get_bg_color $w]\n-\t\t$w.msg tag configure bold -font font_uibold -justify center\n-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n-\t\t$w.msg insert end $s bold \\n\\n$err {}\n-\t\t$w.msg configure -state disabled\n-\n-\t\t${NS}::button $w.continue \\\n-\t\t\t-text [mc \"Continue\"] \\\n-\t\t\t-command [list destroy $w]\n-\t\t${NS}::button $w.unlock \\\n-\t\t\t-text [mc \"Unlock Index\"] \\\n-\t\t\t-command \"destroy $w; _delete_indexlock\"\n-\t\tgrid $w.msg - $w.vs -sticky news\n-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n-\t\tgrid columnconfigure $w 0 -weight 1\n-\t\tgrid rowconfigure $w 0 -weight 1\n-\n-\t\twm protocol $w WM_DELETE_WINDOW update\n-\t\tbind $w.continue <Visibility> \"\n-\t\t\tgrab $w\n-\t\t\tfocus %W\n-\t\t\"\n-\t\twm deiconify $w\n-\t\ttkwait window $w\n-\n-\t\t$::main_status stop\n+# Returns true if the operation succeeded, false if a rescan has been initiated.\n+proc close_and_unlock_updateindex_rescan_on_error {fd after} {\n+\tif {![catch {_close_updateindex $fd} err]} {\n \t\tunlock_index\n-\t\trescan $after 0\n-\t\treturn\n+\t\tuplevel #0 $after\n+\t\treturn 1\n+\t} else {\n+\t\trescan_on_error $err $after\n+\t\treturn 0\n \t}\n+}\n \n-\t$::main_status stop\n+proc _close_updateindex {fd} {\n+\tfconfigure $fd -blocking 1\n+\tclose $fd\n+}\n+\n+proc rescan_on_error {err {after {}}} {\n+\tglobal use_ttk NS\n+\n+\tset w .indexfried\n+\tDialog $w\n+\twm withdraw $w\n+\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n+\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n+\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n+\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n+\t\t-width [string length $s] -relief flat \\\n+\t\t-borderwidth 0 -highlightthickness 0 \\\n+\t\t-background [get_bg_color $w]\n+\t$w.msg tag configure bold -font font_uibold -justify center\n+\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n+\t$w.msg insert end $s bold \\n\\n$err {}\n+\t$w.msg configure -state disabled\n+\n+\t${NS}::button $w.continue \\\n+\t\t-text [mc \"Continue\"] \\\n+\t\t-command [list destroy $w]\n+\t${NS}::button $w.unlock \\\n+\t\t-text [mc \"Unlock Index\"] \\\n+\t\t-command \"destroy $w; _delete_indexlock\"\n+\tgrid $w.msg - $w.vs -sticky news\n+\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n+\tgrid columnconfigure $w 0 -weight 1\n+\tgrid rowconfigure $w 0 -weight 1\n+\n+\twm protocol $w WM_DELETE_WINDOW update\n+\tbind $w.continue <Visibility> \"\n+\t\tgrab $w\n+\t\tfocus %W\n+\t\"\n+\twm deiconify $w\n+\ttkwait window $w\n+\n+\t$::main_status stop_all\n \tunlock_index\n-\tuplevel #0 $after\n+\trescan [concat $after [list ui_ready]] 0\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -67,7 +77,7 @@ proc update_indexinfo {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index -z --index-info]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -81,16 +91,19 @@ proc update_indexinfo {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd path_list total_cnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t$status_bar_operation stop\n+\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n \t\treturn\n \t}\n \n@@ -119,7 +132,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc update_index {msg path_list after} {\n@@ -133,7 +146,7 @@ proc update_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index --add --remove -z --stdin]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -147,16 +160,19 @@ proc update_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd path_list total_cnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t$status_bar_operation stop\n+\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n \t\treturn\n \t}\n \n@@ -190,10 +206,10 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg path_list after} {\n+proc checkout_index {msg path_list after capture_error} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n@@ -204,7 +220,7 @@ proc checkout_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write checkout-index \\\n \t\t--index \\\n \t\t--quiet \\\n@@ -224,16 +240,26 @@ proc checkout_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n+\t\t$capture_error \\\n \t\t]\n }\n \n-proc write_checkout_index {fd path_list total_cnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter capture_error} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n-\t\t_close_updateindex $fd $after\n+\t\t$status_bar_operation stop\n+\n+\t\tif {[catch {_close_updateindex $fd} err]} {\n+\t\t\tuplevel #0 $capture_error [list $err]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\n \t\treturn\n \t}\n \n@@ -253,7 +279,7 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -388,66 +414,301 @@ proc do_add_all {} {\n \tadd_helper [mc \"Adding all changed files\"] $paths\n }\n \n+# Copied from TclLib package \"lambda\".\n+proc lambda {arguments body args} {\n+\treturn [list ::apply [list $arguments $body] {*}$args]\n+}\n+\n proc revert_helper {txt paths} {\n \tglobal file_states current_diff_path\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\t#\n+\t# The asynchronous operations are each indicated below by a comment\n+\t# before the code block that starts the async operation.\n+\tset after_chord [SimpleChord new {\n+\t\tif {[string trim $err] != \"\"} {\n+\t\t\trescan_on_error $err\n+\t\t} else {\n+\t\t\tunlock_index\n+\t\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\t\tui_ready\n+\t\t}\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# This function captures an error for processing when after_chord is\n+\t# completed. (The chord is curried into the lambda function.)\n+\tset capture_error [lambda \\\n+\t\t{chord error} \\\n+\t\t{ $chord eval [list set err $error] } \\\n+\t\t$after_chord]\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n-\tset after {}\n+\tset untracked_list [list]\n+\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n+\n+\t# Asynchronous operation: revert changes by checking them out afresh\n+\t# from the index.\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note] \\\n+\t\t\t\t$capture_error\n+\t\t}\n \t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t# Asynchronous operation: Deletion of untracked files.\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n \n-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\tset status_bar_operation [$::main_status \\\n+\t\tstart \\\n+\t\t[mc \"Deleting\"] \\\n+\t\t[mc \"files\"]]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$status_bar_operation \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size \\\n+\tstatus_bar_operation after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$status_bar_operation update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$status_bar_operation \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$status_bar_operation stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0)\n+\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \ndiff --git a/lib/merge.tcl b/lib/merge.tcl\nindex 9f253db5b3..8df8ffae55 100644\n--- a/lib/merge.tcl\n+++ b/lib/merge.tcl\n@@ -241,23 +241,27 @@ Continue with resetting the current changes?\"]\n \tif {[ask_popup $op_question] eq {yes}} {\n \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n \t\tfconfigure $fd -blocking 0 -translation binary\n-\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n-\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n+\t\tset status_bar_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Aborting\"] \\\n+\t\t\t[mc \"files reset\"]\n+\t\tfileevent $fd readable [namespace code [list \\\n+\t\t\t_reset_wait $fd $status_bar_operation]]\n \t} else {\n \t\tunlock_index\n \t}\n }\n \n-proc _reset_wait {fd} {\n+proc _reset_wait {fd status_bar_operation} {\n \tglobal ui_comm\n \n-\t$::main_status update_meter [read $fd]\n+\t$status_bar_operation update_meter [read $fd]\n \n \tfconfigure $fd -blocking 1\n \tif {[eof $fd]} {\n \t\tset fail [catch {close $fd} err]\n-\t\t$::main_status stop\n \t\tunlock_index\n+\t\t$status_bar_operation stop\n \n \t\t$ui_comm delete 0.0 end\n \t\t$ui_comm edit modified false\ndiff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\nindex 02111a1742..6a73988b23 100644\n--- a/lib/status_bar.tcl\n+++ b/lib/status_bar.tcl\n@@ -1,16 +1,42 @@\n # git-gui status bar mega-widget\n # Copyright (C) 2007 Shawn Pearce\n \n+# The status_bar class manages the entire status bar. It is possible for\n+# multiple overlapping asynchronous operations to want to display status\n+# simultaneously. Each one receives a status_bar_operation when it calls the\n+# start method, and the status bar combines all active operations into the\n+# line of text it displays. Most of the time, there will be at most one\n+# ongoing operation.\n+#\n+# Note that the entire status bar can be either in single-line or two-line\n+# mode, depending on the constructor. Multiple active operations are only\n+# supported for single-line status bars.\n+\n class status_bar {\n \n+field allow_multiple ; # configured at construction\n+\n field w         ; # our own window path\n field w_l       ; # text widget we draw messages into\n field w_c       ; # canvas we draw a progress bar into\n field c_pack    ; # script to pack the canvas with\n-field status  {}; # single line of text we show\n-field prefix  {}; # text we format into status\n-field units   {}; # unit of progress\n-field meter   {}; # current core git progress meter (if active)\n+\n+field baseline_text   ; # text to show if there are no operations\n+field status_bar_text ; # combined text for all operations\n+\n+field operations ; # list of current ongoing operations\n+\n+# The status bar can display a progress bar, updated when consumers call the\n+# update method on their status_bar_operation. When there are multiple\n+# operations, the status bar shows the combined status of all operations.\n+#\n+# When an overlapping operation completes, the progress bar is going to\n+# abruptly have one fewer operation in the calculation, causing a discontinuity.\n+# Therefore, whenever an operation completes, if it is not the last operation,\n+# this counter is increased, and the progress bar is calculated as though there\n+# were still another operation at 100%. When the last operation completes, this\n+# is reset to 0.\n+field completed_operation_count\n \n constructor new {path} {\n \tglobal use_ttk NS\n@@ -18,12 +44,19 @@ constructor new {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Standard single-line status bar: Permit overlapping operations\n+\tset allow_multiple 1\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \tif {!$use_ttk} {\n \t\t$w configure -borderwidth 1 -relief sunken\n \t}\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -side left\n@@ -44,9 +77,15 @@ constructor two_line {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Two-line status bar: Only one ongoing operation permitted.\n+\tset allow_multiple 0\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\n \t${NS}::frame $w\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -anchor w -fill x\n@@ -56,7 +95,7 @@ constructor two_line {path} {\n \treturn $this\n }\n \n-method start {msg uds} {\n+method ensure_canvas {} {\n \tif {[winfo exists $w_c]} {\n \t\t$w_c coords bar 0 0 0 20\n \t} else {\n@@ -68,31 +107,169 @@ method start {msg uds} {\n \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n \t\teval $c_pack\n \t}\n+}\n+\n+method show {msg {test {}}} {\n+\tif {$test eq {} || $status eq $test} {\n+\t\t$this ensure_canvas\n+\t\tset baseline_text $msg\n+\t\t$this refresh\n+\t}\n+}\n+\n+method start {msg uds} {\n+\tset baseline_text \"\"\n+\n+\tif {!$allow_multiple && [llength $operations]} {\n+\t\treturn [lindex $operations 0]\n+\t}\n+\n+\t$this ensure_canvas\n+\n+\tset operation [status_bar_operation::new $this $msg $uds]\n+\n+\tlappend operations $operation\n+\n+\t$this refresh\n+\n+\treturn $operation\n+}\n+\n+method refresh {} {\n+\tset new_text \"\"\n+\n+\tset total [expr $completed_operation_count * 100]\n+\tset have $total\n+\n+\tforeach operation $operations {\n+\t\tif {$new_text != \"\"} {\n+\t\t\tappend new_text \" / \"\n+\t\t}\n+\n+\t\tappend new_text [$operation get_status]\n+\n+\t\tset total [expr $total + 100]\n+\t\tset have [expr $have + [$operation get_progress]]\n+\t}\n+\n+\tif {$new_text == \"\"} {\n+\t\tset new_text $baseline_text\n+\t}\n+\n+\tset status_bar_text $new_text\n+\n+\tset pixel_width 0\n+\tif {$have > 0} {\n+\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n+\t}\n+\n+\t$w_c coords bar 0 0 $pixel_width 20\n+}\n+\n+method stop {operation stop_msg} {\n+\tset idx [lsearch $operations $operation]\n+\n+\tif {$idx >= 0} {\n+\t\tset operations [lreplace $operations $idx $idx]\n+\t\tset completed_operation_count [expr \\\n+\t\t\t$completed_operation_count + 1]\n+\n+\t\tif {[llength operations] == 0} {\n+\t\t\tset completed_operation_count 0\n+\n+\t\t\tdestroy $w_c\n+\t\t\tif {$stop_msg ne {}} {\n+\t\t\t\tset baseline_text $stop_msg\n+\t\t\t}\n+\t\t}\n+\n+\t\t$this refresh\n+\t}\n+}\n+\n+method stop_all {{stop_msg {}}} {\n+\tset operations_copy $operations\n+\tset operations [list] # This makes the operation's call to stop a no-op.\n+\n+\tforeach $operation operations_copy {\n+\t\t$operation stop\n+\t}\n+\n+\tif {$stop_msg ne {}} {\n+\t\tset baseline_text $stop_msg\n+\t}\n+\n+\t$this refresh\n+}\n+\n+method _delete {current} {\n+\tif {$current eq $w} {\n+\t\tdelete_this\n+\t}\n+}\n+\n+}\n+\n+# The status_bar_operation class tracks a single consumer's ongoing status bar\n+# activity, with the context that there are a few situations where multiple\n+# overlapping asynchronous operations might want to display status information\n+# simultaneously. Instances of status_bar_operation are created by calling\n+# start on the status_bar, and when the caller is done with its stauts bar\n+# operation, it calls stop on the operation.\n+\n+class status_bar_operation {\n+\n+field status_bar; # reference back to the status_bar that owns this object\n+\n+field is_active;\n+\n+field status   {}; # single line of text we show\n+field progress {}; # current progress (0 to 100)\n+field prefix   {}; # text we format into status\n+field units    {}; # unit of progress\n+field meter    {}; # current core git progress meter (if active)\n+\n+constructor new {owner msg uds} {\n+\tset status_bar $owner\n \n \tset status $msg\n+\tset progress 0\n \tset prefix $msg\n \tset units  $uds\n \tset meter  {}\n+\n+\tset is_active 1\n+\n+\treturn $this\n }\n \n+method get_is_active {} { return $is_active }\n+method get_status {} { return $status }\n+method get_progress {} { return $progress }\n+\n method update {have total} {\n-\tset pdone 0\n-\tset cdone 0\n+\tif {!$is_active} { return }\n+\n+\tset progress 0\n+\n \tif {$total > 0} {\n-\t\tset pdone [expr {100 * $have / $total}]\n-\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n+\t\tset progress [expr {100 * $have / $total}]\n \t}\n \n \tset prec [string length [format %i $total]]\n+\n \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n \t\t$prefix \\\n \t\t$prec $have \\\n \t\t$prec $total \\\n-\t\t$units $pdone]\n-\t$w_c coords bar 0 0 $cdone 20\n+\t\t$units $progress]\n+\n+\t$status_bar refresh\n }\n \n method update_meter {buf} {\n+\tif {!$is_active} { return }\n+\n \tappend meter $buf\n \tset r [string last \"\\r\" $meter]\n \tif {$r == -1} {\n@@ -109,23 +286,25 @@ method update_meter {buf} {\n \t}\n }\n \n-method stop {{msg {}}} {\n-\tdestroy $w_c\n-\tif {$msg ne {}} {\n-\t\tset status $msg\n+method stop {{stop_msg {}}} {\n+\tif {$is_active} {\n+\t\tset is_active 0\n+\t\t$status_bar stop $this $stop_msg\n \t}\n }\n \n method show {msg {test {}}} {\n+\tif {!$is_active} { return }\n+\n \tif {$test eq {} || $status eq $test} {\n \t\tset status $msg\n+\t\t$status_bar refresh\n \t}\n }\n \n-method _delete {current} {\n-\tif {$current eq $w} {\n-\t\tdelete_this\n-\t}\n+method _delete {} {\n+\tstop\n+\tdelete_this\n }\n \n }\n-- \ngitgitgadget\n"},{"id":"386521","messageId":"20191119152156.tzsh2hxgcgcegvtt@yadavpratyush.com","threadId":"52154","inReplyTo":"pull.436.v4.git.1573973770.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-19T15:21:56Z","receivedAt":"2019-11-19T15:22:09Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"On 17/11/19 06:56AM, Jonathan Gilbert via GitGitGadget wrote:\n>  * The initialization code in git-gui.sh (which I'm assuming \n>  translates\n>    somehow to git-gui.tcl in the installation?) now explicitly clears the\n\nWell, the design is a bit strange. git-gui.sh happens to be _both_ a \nshell script and a Tcl script. When you run './git-gui.sh', it is \nexecuted as a shell script. That shell script then executes itself via \n'wish' (which is the Tcl/Tk windowing shell), and Tcl ignores the first \n\"line\" (it is actually the lines 3-10, but they all have an escaped \nnewline so it is effectively a single line).\n\nIt has been like since the very first revision of git-gui. I wonder why \nthe original author went with this instead of just doing something like:\n\n  #!/usr/bin/env wish\n\nwhich seems to work just fine on my quick testing, but that is another \ntopic entirely ;)\n\n>    \"Initializing...\" status bar text, since the new status bar model won't\n>    do this automatically when operations are performed.\n\nPS: I am in the process of reviewing the latest revision of the patch \nseries. But I have been short on free time recently so it might take me \na couple more days.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"386535","messageId":"CAPSOpYvEnWEg8JDyj2ASd-igh6Q41=BUinq1yxwev-DJqyfyWA@mail.gmail.com","threadId":"52154","inReplyTo":"20191119152156.tzsh2hxgcgcegvtt@yadavpratyush.com","subject":"Re: [PATCH v4 0/2] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-19T16:56:43Z","receivedAt":"2019-11-19T16:56:59Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Tue, Nov 19, 2019 at 9:22 AM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> On 17/11/19 06:56AM, Jonathan Gilbert via GitGitGadget wrote:\n> >  * The initialization code in git-gui.sh (which I'm assuming\n> >     translates somehow to git-gui.tcl in the installation?)\n>\n> Well, the design is a bit strange. git-gui.sh happens to be _both_ a\n> shell script and a Tcl script.\n\nAh, I see -- I had managed to convince myself that they were different\nfiles, but never actually diffed them. In my installation, I have a\nfile \"C:\\Program Files\\Git\\mingw64\\libexec\\git-core\\git-gui.tcl\" and\nthat _is_ the same file as git-gui.sh in the repository. So maybe it's\nWindows-specific, maybe not, I'm not sure, but at some point it gets\nrenamed from git-gui.sh to git-gui.tcl. I incorrectly assumed that the\n.tcl file didn't have all the facets of the .sh file. :-P\n\nThanks,\n\nJonathan Gilbert\n"},{"id":"386939","messageId":"20191124130845.eawt4hg73hth2uro@yadavpratyush.com","threadId":"52154","inReplyTo":"23d4f5d829cb705441394186d274620b46bae8fb.1573973770.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/2] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-24T13:09:43Z","receivedAt":"2019-11-24T13:10:07Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the re-roll. Sorry for taking so long to review. I couldn't \nfind too much free time past few days.\n\nOn 17/11/19 06:56AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the revert_helper proc to check for untracked files as well as\n> changes, and then handle changes to be reverted and untracked files with\n> independent blocks of code. Prompt the user independently for untracked\n> files, since the underlying action is fundamentally different (rm -f).\n> If after deleting untracked files, the directory containing them becomes\n> empty, then remove the directory as well. Migrate unlocking of the index\n> out of _close_updateindex to a responsibility of the caller, to permit\n> paths that don't directly unlock the index, and refactor the error\n> handling added in d4e890e5 so that callers can make flow control\n> decisions in the event of errors. Rework status_bar to explicitly handle\n> multiple overlapping operations, and update all call sites.\n> \n> A new proc delete_files takes care of actually deleting the files in\n> batches, using the Tcler's Wiki recommended approach for keeping the UI\n> responsive.\n> \n> Since the checkout_index and delete_files calls are both asynchronous\n> and could potentially complete in any order, a \"chord\" is used to\n> coordinate unlocking the index and returning the UI to a usable state\n> only after both operations are complete. The `SimpleChord` class,\n> based on TclOO (Tcl/Tk 8.6), is added in this commit.\n> \n> Since the checkout_index and delete_files calls are both asynchronous\n> and overlap, they clash in wanting to update the status bar. This commit\n> reworks the status bar so that when an operation wants to display\n> ongoing updates/progress, it explicitly starts an \"operation\", which is\n> tracked by its own object, and the status bar handles multiple\n> concurrent operations by merging their progress and concatenating their\n> text.\n\nThe status bar is a major change, so I think it should be in a separate \ncommit. That would make it easier to debug it when bisecting, and to \nrevert it in case we discover a bug later.\n\nUnfortunately, this change breaks things. The users of 'status_bar' that \naren't updated don't work. As an example, if I run 'git gui blame \ngit-gui.sh' with your patch applied, I get the following error:\n\n  invalid command name \"::status_bar::update\"\n  invalid command name \"::status_bar::update\"\n      while executing\n  \"::status_bar::update ::status_bar::__o1::__d 5 4169\"\n      (\"eval\" body line 1)\n      invoked from within\n  \"eval [list ::status_bar::$name ::status_bar::__o1::__d] $args\"\n      (procedure \"::status_bar::__o1::__d\" line 1)\n      invoked from within\n  \"$status update $blame_lines [set ${__this}::total_lines]\"\n      (procedure \"blame::_read_blame\" line 184)\n      invoked from within\n  \"blame::_read_blame ::blame::__o1::__d file7 .file_pane.out.asimple_t ::blame::__o1::asim_data\"\n\nThis error is raised because of lib/blame.tcl:812, and causes blame to \nnot annotate lines properly, which is the entire reason to use blame.\n\nAnother caller that would probably break is 'lib/choose_repository.tcl'. \nThese are the only two broken callsites I can spot after some quick \nlooking around.\n\nAre there any other backward-compatibility breaking changes to \nstatus_bar? I have a feeling that this changeset is already getting a \nbit too large in scope. Maybe we should figure out a simpler compromise \ninstead of making a huge re-work like this.\n\nIf the existing callsites can be updated without much trouble, then its \nfine. Otherwise, maybe reducing the scope of this change would be a \nbetter idea.\n\nEither way, please split the status bar refactor in a separate commit \nsuch that the entire system still works properly (so this means the \ncommit would include updating the existing callsites).\n \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  git-gui.sh          |   7 +-\n>  lib/checkout_op.tcl |  15 +-\n>  lib/chord.tcl       | 160 ++++++++++++++++\n>  lib/index.tcl       | 443 +++++++++++++++++++++++++++++++++++---------\n>  lib/merge.tcl       |  14 +-\n>  lib/status_bar.tcl  | 221 +++++++++++++++++++---\n>  6 files changed, 734 insertions(+), 126 deletions(-)\n>  create mode 100644 lib/chord.tcl\n> \n> diff --git a/git-gui.sh b/git-gui.sh\n> index 0d21f5688b..dc4ac577ac 100755\n> --- a/git-gui.sh\n> +++ b/git-gui.sh\n> @@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n>  ##\n>  ## Tcl/Tk sanity check\n>  \n> -if {[catch {package require Tcl 8.4} err]\n> - || [catch {package require Tk  8.4} err]\n> +if {[catch {package require Tcl 8.6} err]\n> + || [catch {package require Tk  8.6} err]\n>  } {\n>  \tcatch {wm withdraw .}\n>  \ttk_messageBox \\\n> @@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n>  \tdo_explore\n>  }\n>  \n> +# Clear \"Initializing...\" status\n> +after idle {after 500 {$main_status show \"\"}}\n\nWhy put this in an 'after idle'? What's wrong with just 'after 500'? \nThis is not an expensive operation so we shouldn't really require the \napplication to be idle to run it.\n\n> +\n>  # Local variables:\n>  # mode: tcl\n>  # indent-tabs-mode: t\n> diff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\n> index a5228297db..21ea768d80 100644\n> --- a/lib/checkout_op.tcl\n> +++ b/lib/checkout_op.tcl\n> @@ -341,9 +341,9 @@ method _readtree {} {\n>  \tglobal HEAD\n>  \n>  \tset readtree_d {}\n> -\t$::main_status start \\\n> +\tset status_bar_operation [$::main_status start \\\n>  \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n> -\t\t[mc \"files checked out\"]\n> +\t\t[mc \"files checked out\"]]\n>  \n>  \tset fd [git_read --stderr read-tree \\\n>  \t\t-m \\\n> @@ -354,26 +354,27 @@ method _readtree {} {\n>  \t\t$new_hash \\\n>  \t\t]\n>  \tfconfigure $fd -blocking 0 -translation binary\n> -\tfileevent $fd readable [cb _readtree_wait $fd]\n> +\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n>  }\n>  \n> -method _readtree_wait {fd} {\n> +method _readtree_wait {fd status_bar_operation} {\n>  \tglobal current_branch\n>  \n>  \tset buf [read $fd]\n> -\t$::main_status update_meter $buf\n> +\t$status_bar_operation update_meter $buf\n>  \tappend readtree_d $buf\n>  \n>  \tfconfigure $fd -blocking 1\n>  \tif {![eof $fd]} {\n>  \t\tfconfigure $fd -blocking 0\n> +\t\t$status_bar_operation stop\n>  \t\treturn\n>  \t}\n>  \n>  \tif {[catch {close $fd}]} {\n>  \t\tset err $readtree_d\n>  \t\tregsub {^fatal: } $err {} err\n> -\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n> +\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n>  \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n>  \n>  $err\n> @@ -384,7 +385,7 @@ $err\n>  \t\treturn\n>  \t}\n>  \n> -\t$::main_status stop\n> +\t$status_bar_operation stop\n>  \t_after_readtree $this\n>  }\n>  \n> diff --git a/lib/index.tcl b/lib/index.tcl\n> index 28d4d2a54e..8d7590241e 100644\n> --- a/lib/index.tcl\n> +++ b/lib/index.tcl\n> @@ -7,53 +7,63 @@ proc _delete_indexlock {} {\n>  \t}\n>  }\n>  \n> -proc _close_updateindex {fd after} {\n> -\tglobal use_ttk NS\n> -\tfconfigure $fd -blocking 1\n> -\tif {[catch {close $fd} err]} {\n> -\t\tset w .indexfried\n> -\t\tDialog $w\n> -\t\twm withdraw $w\n> -\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n> -\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n> -\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n> -\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n> -\t\t\t-width [string length $s] -relief flat \\\n> -\t\t\t-borderwidth 0 -highlightthickness 0 \\\n> -\t\t\t-background [get_bg_color $w]\n> -\t\t$w.msg tag configure bold -font font_uibold -justify center\n> -\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n> -\t\t$w.msg insert end $s bold \\n\\n$err {}\n> -\t\t$w.msg configure -state disabled\n> -\n> -\t\t${NS}::button $w.continue \\\n> -\t\t\t-text [mc \"Continue\"] \\\n> -\t\t\t-command [list destroy $w]\n> -\t\t${NS}::button $w.unlock \\\n> -\t\t\t-text [mc \"Unlock Index\"] \\\n> -\t\t\t-command \"destroy $w; _delete_indexlock\"\n> -\t\tgrid $w.msg - $w.vs -sticky news\n> -\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n> -\t\tgrid columnconfigure $w 0 -weight 1\n> -\t\tgrid rowconfigure $w 0 -weight 1\n> -\n> -\t\twm protocol $w WM_DELETE_WINDOW update\n> -\t\tbind $w.continue <Visibility> \"\n> -\t\t\tgrab $w\n> -\t\t\tfocus %W\n> -\t\t\"\n> -\t\twm deiconify $w\n> -\t\ttkwait window $w\n> -\n> -\t\t$::main_status stop\n> +# Returns true if the operation succeeded, false if a rescan has been initiated.\n> +proc close_and_unlock_updateindex_rescan_on_error {fd after} {\n\nNitpick: That name is a bit too unwieldy. Maybe something a bit more \nconcise like 'close_and_unlock_index' (I'm not great at naming things. \nMaybe you can figure out something better)? Let the readers figure out \nwhat happens on error.\n\n> +\tif {![catch {_close_updateindex $fd} err]} {\n>  \t\tunlock_index\n> -\t\trescan $after 0\n> -\t\treturn\n> +\t\tuplevel #0 $after\n> +\t\treturn 1\n> +\t} else {\n> +\t\trescan_on_error $err $after\n> +\t\treturn 0\n\nNeither of the two callers use the return value. Are these really \nneeded?\n\n>  \t}\n> +}\n>  \n> -\t$::main_status stop\n> +proc _close_updateindex {fd} {\n> +\tfconfigure $fd -blocking 1\n> +\tclose $fd\n> +}\n> +\n> +proc rescan_on_error {err {after {}}} {\n> +\tglobal use_ttk NS\n> +\n> +\tset w .indexfried\n> +\tDialog $w\n> +\twm withdraw $w\n> +\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n> +\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n> +\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n> +\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n> +\t\t-width [string length $s] -relief flat \\\n> +\t\t-borderwidth 0 -highlightthickness 0 \\\n> +\t\t-background [get_bg_color $w]\n> +\t$w.msg tag configure bold -font font_uibold -justify center\n> +\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n> +\t$w.msg insert end $s bold \\n\\n$err {}\n> +\t$w.msg configure -state disabled\n> +\n> +\t${NS}::button $w.continue \\\n> +\t\t-text [mc \"Continue\"] \\\n> +\t\t-command [list destroy $w]\n> +\t${NS}::button $w.unlock \\\n> +\t\t-text [mc \"Unlock Index\"] \\\n> +\t\t-command \"destroy $w; _delete_indexlock\"\n> +\tgrid $w.msg - $w.vs -sticky news\n> +\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n> +\tgrid columnconfigure $w 0 -weight 1\n> +\tgrid rowconfigure $w 0 -weight 1\n> +\n> +\twm protocol $w WM_DELETE_WINDOW update\n> +\tbind $w.continue <Visibility> \"\n> +\t\tgrab $w\n> +\t\tfocus %W\n> +\t\"\n> +\twm deiconify $w\n> +\ttkwait window $w\n> +\n> +\t$::main_status stop_all\n>  \tunlock_index\n> -\tuplevel #0 $after\n> +\trescan [concat $after [list ui_ready]] 0\n>  }\n>  \n>  proc update_indexinfo {msg path_list after} {\n> @@ -67,7 +77,7 @@ proc update_indexinfo {msg path_list after} {\n>  \tset batch [expr {int($total_cnt * .01) + 1}]\n>  \tif {$batch > 25} {set batch 25}\n>  \n> -\t$::main_status start $msg [mc \"files\"]\n> +\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n>  \tset fd [git_write update-index -z --index-info]\n>  \tfconfigure $fd \\\n>  \t\t-blocking 0 \\\n> @@ -81,16 +91,19 @@ proc update_indexinfo {msg path_list after} {\n>  \t\t$path_list \\\n>  \t\t$total_cnt \\\n>  \t\t$batch \\\n> +\t\t$status_bar_operation \\\n>  \t\t$after \\\n>  \t\t]\n>  }\n>  \n> -proc write_update_indexinfo {fd path_list total_cnt batch after} {\n> +proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n> +\tafter} {\n>  \tglobal update_index_cp\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t$status_bar_operation stop\n> +\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n>  \t\treturn\n>  \t}\n>  \n> @@ -119,7 +132,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n>  \t\tdisplay_file $path $new\n>  \t}\n>  \n> -\t$::main_status update $update_index_cp $total_cnt\n> +\t$status_bar_operation update $update_index_cp $total_cnt\n>  }\n>  \n>  proc update_index {msg path_list after} {\n> @@ -133,7 +146,7 @@ proc update_index {msg path_list after} {\n>  \tset batch [expr {int($total_cnt * .01) + 1}]\n>  \tif {$batch > 25} {set batch 25}\n>  \n> -\t$::main_status start $msg [mc \"files\"]\n> +\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n>  \tset fd [git_write update-index --add --remove -z --stdin]\n>  \tfconfigure $fd \\\n>  \t\t-blocking 0 \\\n> @@ -147,16 +160,19 @@ proc update_index {msg path_list after} {\n>  \t\t$path_list \\\n>  \t\t$total_cnt \\\n>  \t\t$batch \\\n> +\t\t$status_bar_operation \\\n>  \t\t$after \\\n>  \t\t]\n>  }\n>  \n> -proc write_update_index {fd path_list total_cnt batch after} {\n> +proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n> +\tafter} {\n>  \tglobal update_index_cp\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t$status_bar_operation stop\n> +\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n>  \t\treturn\n>  \t}\n>  \n> @@ -190,10 +206,10 @@ proc write_update_index {fd path_list total_cnt batch after} {\n>  \t\tdisplay_file $path $new\n>  \t}\n>  \n> -\t$::main_status update $update_index_cp $total_cnt\n> +\t$status_bar_operation update $update_index_cp $total_cnt\n>  }\n>  \n> -proc checkout_index {msg path_list after} {\n> +proc checkout_index {msg path_list after capture_error} {\n>  \tglobal update_index_cp\n>  \n>  \tif {![lock_index update]} return\n> @@ -204,7 +220,7 @@ proc checkout_index {msg path_list after} {\n>  \tset batch [expr {int($total_cnt * .01) + 1}]\n>  \tif {$batch > 25} {set batch 25}\n>  \n> -\t$::main_status start $msg [mc \"files\"]\n> +\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n>  \tset fd [git_write checkout-index \\\n>  \t\t--index \\\n>  \t\t--quiet \\\n> @@ -224,16 +240,26 @@ proc checkout_index {msg path_list after} {\n>  \t\t$path_list \\\n>  \t\t$total_cnt \\\n>  \t\t$batch \\\n> +\t\t$status_bar_operation \\\n>  \t\t$after \\\n> +\t\t$capture_error \\\n>  \t\t]\n>  }\n>  \n> -proc write_checkout_index {fd path_list total_cnt batch after} {\n> +proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n> +\tafter capture_error} {\n>  \tglobal update_index_cp\n>  \tglobal file_states current_diff_path\n>  \n>  \tif {$update_index_cp >= $total_cnt} {\n> -\t\t_close_updateindex $fd $after\n> +\t\t$status_bar_operation stop\n> +\n> +\t\tif {[catch {_close_updateindex $fd} err]} {\n\nNitpick: Please mention exactly why we don't use \n'close_and_unlock_updateindex_rescan_on_error' (or whatever its new name \nwould be) here. This function is very similar to 'write_update_index' \nand 'write_checkout_index', so this subtle difference is not very easily \napparent.\n\n> +\t\t\tuplevel #0 $capture_error [list $err]\n> +\t\t}\n> +\n> +\t\tuplevel #0 $after\n> +\n>  \t\treturn\n>  \t}\n>  \n> diff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\n> index 02111a1742..6a73988b23 100644\n> --- a/lib/status_bar.tcl\n> +++ b/lib/status_bar.tcl\n> @@ -1,16 +1,42 @@\n>  # git-gui status bar mega-widget\n>  # Copyright (C) 2007 Shawn Pearce\n>  \n> +# The status_bar class manages the entire status bar. It is possible for\n> +# multiple overlapping asynchronous operations to want to display status\n> +# simultaneously. Each one receives a status_bar_operation when it calls the\n> +# start method, and the status bar combines all active operations into the\n> +# line of text it displays. Most of the time, there will be at most one\n> +# ongoing operation.\n> +#\n> +# Note that the entire status bar can be either in single-line or two-line\n> +# mode, depending on the constructor. Multiple active operations are only\n> +# supported for single-line status bars.\n> +\n>  class status_bar {\n>  \n> +field allow_multiple ; # configured at construction\n> +\n>  field w         ; # our own window path\n>  field w_l       ; # text widget we draw messages into\n>  field w_c       ; # canvas we draw a progress bar into\n>  field c_pack    ; # script to pack the canvas with\n> -field status  {}; # single line of text we show\n\nThe field 'status' is removed, but the procedure 'show' still uses it. \nThe if condition needs to be refactored.\n\n> -field prefix  {}; # text we format into status\n> -field units   {}; # unit of progress\n> -field meter   {}; # current core git progress meter (if active)\n> +\n> +field baseline_text   ; # text to show if there are no operations\n> +field status_bar_text ; # combined text for all operations\n> +\n> +field operations ; # list of current ongoing operations\n> +\n> +# The status bar can display a progress bar, updated when consumers call the\n> +# update method on their status_bar_operation. When there are multiple\n> +# operations, the status bar shows the combined status of all operations.\n> +#\n> +# When an overlapping operation completes, the progress bar is going to\n> +# abruptly have one fewer operation in the calculation, causing a discontinuity.\n> +# Therefore, whenever an operation completes, if it is not the last operation,\n> +# this counter is increased, and the progress bar is calculated as though there\n> +# were still another operation at 100%. When the last operation completes, this\n> +# is reset to 0.\n> +field completed_operation_count\n>  \n>  constructor new {path} {\n>  \tglobal use_ttk NS\n> @@ -18,12 +44,19 @@ constructor new {path} {\n>  \tset w_l $w.l\n>  \tset w_c $w.c\n>  \n> +\t# Standard single-line status bar: Permit overlapping operations\n> +\tset allow_multiple 1\n> +\n> +\tset baseline_text \"\"\n> +\tset operations [list]\n> +\tset completed_operation_count 0\n> +\n>  \t${NS}::frame $w\n>  \tif {!$use_ttk} {\n>  \t\t$w configure -borderwidth 1 -relief sunken\n>  \t}\n>  \t${NS}::label $w_l \\\n> -\t\t-textvariable @status \\\n> +\t\t-textvariable @status_bar_text \\\n>  \t\t-anchor w \\\n>  \t\t-justify left\n>  \tpack $w_l -side left\n> @@ -44,9 +77,15 @@ constructor two_line {path} {\n>  \tset w_l $w.l\n>  \tset w_c $w.c\n>  \n> +\t# Two-line status bar: Only one ongoing operation permitted.\n> +\tset allow_multiple 0\n> +\n> +\tset baseline_text \"\"\n> +\tset operations [list]\n> +\n>  \t${NS}::frame $w\n>  \t${NS}::label $w_l \\\n> -\t\t-textvariable @status \\\n> +\t\t-textvariable @status_bar_text \\\n>  \t\t-anchor w \\\n>  \t\t-justify left\n>  \tpack $w_l -anchor w -fill x\n> @@ -56,7 +95,7 @@ constructor two_line {path} {\n>  \treturn $this\n>  }\n>  \n> -method start {msg uds} {\n> +method ensure_canvas {} {\n>  \tif {[winfo exists $w_c]} {\n>  \t\t$w_c coords bar 0 0 0 20\n>  \t} else {\n> @@ -68,31 +107,169 @@ method start {msg uds} {\n>  \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n>  \t\teval $c_pack\n>  \t}\n> +}\n> +\n> +method show {msg {test {}}} {\n> +\tif {$test eq {} || $status eq $test} {\n> +\t\t$this ensure_canvas\n> +\t\tset baseline_text $msg\n> +\t\t$this refresh\n> +\t}\n> +}\n> +\n> +method start {msg uds} {\n> +\tset baseline_text \"\"\n> +\n> +\tif {!$allow_multiple && [llength $operations]} {\n\nThis silently ignores multiple 'start's on a status bar that doesn't \nallow it, correct?\n\n> +\t\treturn [lindex $operations 0]\n> +\t}\n> +\n> +\t$this ensure_canvas\n> +\n> +\tset operation [status_bar_operation::new $this $msg $uds]\n> +\n> +\tlappend operations $operation\n> +\n> +\t$this refresh\n> +\n> +\treturn $operation\n> +}\n> +\n> +method refresh {} {\n> +\tset new_text \"\"\n> +\n> +\tset total [expr $completed_operation_count * 100]\n> +\tset have $total\n> +\n> +\tforeach operation $operations {\n> +\t\tif {$new_text != \"\"} {\n> +\t\t\tappend new_text \" / \"\n> +\t\t}\n> +\n> +\t\tappend new_text [$operation get_status]\n> +\n> +\t\tset total [expr $total + 100]\n> +\t\tset have [expr $have + [$operation get_progress]]\n> +\t}\n> +\n> +\tif {$new_text == \"\"} {\n> +\t\tset new_text $baseline_text\n> +\t}\n> +\n> +\tset status_bar_text $new_text\n> +\n> +\tset pixel_width 0\n> +\tif {$have > 0} {\n> +\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n> +\t}\n> +\n> +\t$w_c coords bar 0 0 $pixel_width 20\n> +}\n> +\n> +method stop {operation stop_msg} {\n> +\tset idx [lsearch $operations $operation]\n> +\n> +\tif {$idx >= 0} {\n> +\t\tset operations [lreplace $operations $idx $idx]\n> +\t\tset completed_operation_count [expr \\\n> +\t\t\t$completed_operation_count + 1]\n> +\n> +\t\tif {[llength operations] == 0} {\n> +\t\t\tset completed_operation_count 0\n> +\n> +\t\t\tdestroy $w_c\n> +\t\t\tif {$stop_msg ne {}} {\n> +\t\t\t\tset baseline_text $stop_msg\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\t$this refresh\n> +\t}\n> +}\n> +\n> +method stop_all {{stop_msg {}}} {\n> +\tset operations_copy $operations\n> +\tset operations [list] # This makes the operation's call to stop a no-op.\n> +\n> +\tforeach $operation operations_copy {\n> +\t\t$operation stop\n> +\t}\n> +\n> +\tif {$stop_msg ne {}} {\n> +\t\tset baseline_text $stop_msg\n> +\t}\n> +\n> +\t$this refresh\n> +}\n> +\n> +method _delete {current} {\n> +\tif {$current eq $w} {\n> +\t\tdelete_this\n> +\t}\n> +}\n> +\n> +}\n> +\n> +# The status_bar_operation class tracks a single consumer's ongoing status bar\n> +# activity, with the context that there are a few situations where multiple\n> +# overlapping asynchronous operations might want to display status information\n> +# simultaneously. Instances of status_bar_operation are created by calling\n> +# start on the status_bar, and when the caller is done with its stauts bar\n> +# operation, it calls stop on the operation.\n> +\n> +class status_bar_operation {\n> +\n> +field status_bar; # reference back to the status_bar that owns this object\n> +\n> +field is_active;\n> +\n> +field status   {}; # single line of text we show\n> +field progress {}; # current progress (0 to 100)\n> +field prefix   {}; # text we format into status\n> +field units    {}; # unit of progress\n> +field meter    {}; # current core git progress meter (if active)\n> +\n> +constructor new {owner msg uds} {\n> +\tset status_bar $owner\n>  \n>  \tset status $msg\n> +\tset progress 0\n>  \tset prefix $msg\n>  \tset units  $uds\n>  \tset meter  {}\n> +\n> +\tset is_active 1\n> +\n> +\treturn $this\n>  }\n>  \n> +method get_is_active {} { return $is_active }\n> +method get_status {} { return $status }\n> +method get_progress {} { return $progress }\n> +\n>  method update {have total} {\n> -\tset pdone 0\n> -\tset cdone 0\n> +\tif {!$is_active} { return }\n> +\n> +\tset progress 0\n> +\n>  \tif {$total > 0} {\n> -\t\tset pdone [expr {100 * $have / $total}]\n> -\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n> +\t\tset progress [expr {100 * $have / $total}]\n>  \t}\n>  \n>  \tset prec [string length [format %i $total]]\n> +\n>  \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n>  \t\t$prefix \\\n>  \t\t$prec $have \\\n>  \t\t$prec $total \\\n> -\t\t$units $pdone]\n> -\t$w_c coords bar 0 0 $cdone 20\n> +\t\t$units $progress]\n> +\n> +\t$status_bar refresh\n>  }\n>  \n>  method update_meter {buf} {\n> +\tif {!$is_active} { return }\n> +\n>  \tappend meter $buf\n>  \tset r [string last \"\\r\" $meter]\n>  \tif {$r == -1} {\n> @@ -109,23 +286,25 @@ method update_meter {buf} {\n>  \t}\n>  }\n>  \n> -method stop {{msg {}}} {\n> -\tdestroy $w_c\n> -\tif {$msg ne {}} {\n> -\t\tset status $msg\n> +method stop {{stop_msg {}}} {\n> +\tif {$is_active} {\n> +\t\tset is_active 0\n> +\t\t$status_bar stop $this $stop_msg\n>  \t}\n>  }\n>  \n>  method show {msg {test {}}} {\n> +\tif {!$is_active} { return }\n> +\n>  \tif {$test eq {} || $status eq $test} {\n>  \t\tset status $msg\n> +\t\t$status_bar refresh\n>  \t}\n>  }\n>  \n> -method _delete {current} {\n> -\tif {$current eq $w} {\n> -\t\tdelete_this\n> -\t}\n> +method _delete {} {\n> +\tstop\n> +\tdelete_this\n>  }\n>  \n>  }\n\nOne quick question: the consumers of status_bar who don't run multiple \noperations in parallel would still continue working exactly the same \n(after refactoring them to use 'status_bar_operation'), right?\n\nWorks fine on some quick testing, though I haven't done anything too \nthorough. Thanks.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"386953","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1574627876.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v5.git.1574627876.gitgitgadget@gmail.com","subject":"[PATCH v5 1/3] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-24T20:37:53Z","receivedAt":"2019-11-24T20:38:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"386954","messageId":"pull.436.v5.git.1574627876.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v4.git.1573973770.gitgitgadget@gmail.com","subject":"[PATCH v5 0/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-24T20:37:52Z","receivedAt":"2019-11-24T20:38:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nSince the checkout_index and delete_files calls are both asynchronous and\noverlap, they clash in wanting to update the status bar. To address this,\nthe status bar is reworked so that when an operation wants to display\nongoing updates/progress, it explicitly starts an \"operation\", which is\ntracked by its own object, and the status bar handles multiple concurrent\noperations by merging their progress and concatenating their text. This is\ncaptured in a separate commit, since it touches a variety of files.\n\nThe _close_updateindex proc contains error handling (added in d4e890e5) that\nhas the potential to interact badly with unlock_index running at the\ncompletion of an async operation. I have refactored the procedure into\nseparate procs _close_updateindex and rescan_on_error. Call sites that\nexercised the combined functionality also unlocked the index, so a combined\nproc close_and_unlock_index calls _close_updateindex and then either \nrescan_on_error or unlock_index as appropriate. Call sites have been updated\nappropriately.\n\nThe revert_helper proc, with its overlapping operations, is an example of a\ncall site that does not combine the close and unlock/rescan operations. The \ncheckout_index proc has been reworked to only call _close_updateindex, and\nto call a functor supplied by the caller to captures any errors that occur. \nrevert_helper uses this to supply a lambda function that stashes the error\nwithin the chord's body namespace, so that it can then separately call \nrescan_on_error when the chord is completed (or unlock_index, if no error\nwas captured), which might be substantially after checkout_index encounters\nits error. If it turns out that a rescan is called for, it is done once the\ndeletion is complete.\n\nThis is the sixth revision of this change, which differs from the fifth\nversion in the following ways:\n\n * The status_bar.tcl changes have been isolated into a separate commit.\n   \n   \n * A method in status_bar.tcl that apparently had never been hit had some\n   simple bugs in it that have been corrected.\n   \n   \n * The show methods on the classes in status_bar.tcl no longer have a test \n   parameter, as nothing was using that feature that I could see.\n   \n   \n * The refresh method in status_bar.tcl now only tries to update the\n   progress bar widget if it actually exists.\n   \n   \n * blame.tcl had been missed when searching for code using the status bar,\n   this has been addressed.\n   \n   \n * rescan_on_error takes $after as well, in case \n   close_and_unlock_updateindex_rescan_on_error needs to call it.\n   \n   \n * close_and_unlock_updateindex_rescan_on_error has been renamed to simply \n   close_and_unlock_index.\n   \n   \n\nOn Sun, Nov 24, 2019 at 7:09 AM Pratyush Yadav wrote:\n\n> Unfortunately, this change breaks things. The users of 'status_bar' that\naren't updated don't work. As an example, if I run 'git gui blame\ngit-gui.sh' with your patch applied, I get the following error:\n\n invalid command name \"::status_bar::update\"\n\n\nI thought I'd found all places that used the status bar, but apparently\noverlooked blame.tcl. This has been addressed. My apologies!\n\nchoose_repository.tcl was addressed already in the previous commit.\n\n> Either way, please split the status bar refactor in a separate commit such\nthat the entire system still works properly (so this means the commit would\ninclude updating the existing callsites).\n\n\nDone. In doing so and testing it independently, I actually found paths that\nweren't being hit with the full set of changes and fixed a few bugs. In\nparticular, stop_all was completely broken :-P I thought I had exercised it\nbefore and I hadn't.\n\n> > +# Clear \"Initializing...\" status +after idle {after 500 {$main_status show\n\"\"}}\n\n\nWhy put this in an 'after idle'? What's wrong with just 'after 500'? This is\nnot an expensive operation so we shouldn't really require the application to\nbe idle to run it.\n\n\nMy thinking was to make it clear the status bar 500 milliseconds after the\nqueue empties. It's quite possible that my understanding of what it means\nfor the queue to have emptied means that this isn't a terribly meaningful\nthing to do. I was wanting it to continue saying \"Initializing...\" until,\nheuristically, it is done initializing, including any queued up operations.\nI guess if anything waits on I/O, then the queue may to go idle even though\nit's still busy doing things. So, a straight-up after 500 { } would make\nmore sense? I've changed it to this, can make further changes if they are\ncalled for.\n\n> > +# Returns true if the operation succeeded, false if a rescan has been\ninitiated. +proc close_and_unlock_updateindex_rescan_on_error {fd after} {\n\n\nNitpick: That name is a bit too unwieldy. Maybe something a bit more concise\nlike 'close_and_unlock_index' (I'm not great at naming things. Maybe you can\nfigure out something better)? Let the readers figure out what happens on\nerror.\n\n\nGood point. The thinking that had been going on in my head is that \nclose_and_unlock_updateindex_rescan_on_error was combining two operations\nthat most but not all paths would do together, and then the paths that\nneeded them separated would call close_and_unlock in one place and \nrescan_on_error in another. The previous \"cover letter\" actually explicitly\ndescribed this, referring to proc names that actually weren't current any\nmore by the time the code was submitted. Upon closer review, at this point,\nthe place that calls rescan_on_error independently is not making a separate\ncall to something like close_and_unlock, it's just calling \n_close_updateindex directly, and the unlock is occurring in the chord body.\n\n> >  * if {![catch {_close_updateindex $fd} err]} {    unlock_index\n   \n   \n * rescan $after 0\n * return\n * uplevel #0 $after\n * return 1\n * } else {\n * rescan_on_error $err $after\n * return 0\n\n\nNeither of the two callers use the return value. Are these really needed?\n\n\nI'm pretty sure there was an iteration of the code where at least one path\nchecked, but since nothing checks it now, I've removed the return value.\n\n> > +proc write_checkout_index {fd path_list total_cnt batch\nstatus_bar_operation \\\n\n * after capture_error} { global update_index_cp global file_states\n   current_diff_path\n   \n   if {$update_index_cp >= $total_cnt} {\n   \n   \n * _close_updateindex $fd $after\n   \n   \n * $status_bar_operation stop\n   \n   \n * \n * if {[catch {_close_updateindex $fd} err]} {\n   \n   \n\n\nNitpick: Please mention exactly why we don't use\n'close_and_unlock_updateindex_rescan_on_error' (or whatever its new name\nwould be) here. This function is very similar to 'write_update_index' and\n'write_checkout_index', so this subtle difference is not very easily\napparent.\n\n\nDone.\n\n> > -field status {}; # single line of text we show\n\n\nThe field 'status' is removed, but the procedure 'show' still uses it. The\nif condition needs to be refactored.\n\n\nAs far as I can tell, this feature of show to only change the status if the\ntest matches isn't actually used by any call sites. I've removed it. If this\nwas in error, then I'll reintroduce it correctly. I think, generally\nspeaking, supporting overlapping operations resolves the problem that this\nwould have originally resolved, which is that something else has updated the\nstatus since it was previously set and the caller doesn't want to clear\nsomebody else's more up-to-date status text.\n\n> >  * if {!$allow_multiple && [llength $operations]} {\n\n\nThis silently ignores multiple 'start's on a status bar that doesn't allow\nit, correct?\n\n\nIt does, yes. The only status bar that doesn't allow multiple operations is\nthe two-line one created by choose_repository.\n\nA caller that did this erroneously would get the same operation reference\nboth times, which would mean that (presumably) it got stopped multiple times\n--- but stop on a status bar operation is idempotent, so this shouldn't\nactually result in any errors.\n\n> One quick question: the consumers of status_bar who don't run multiple\noperations in parallel would still continue working exactly the same (after\nrefactoring them to use 'status_bar_operation'), right?\n\n\nThis is the expectation, yes, and is what I have observed in my testing. :-)\n\ngit remote add logiclrd https://github.com/logiclrd/git.git\ngit fetch logiclrd git-gui-revert-untracked revision5\ngit diff 23d4f5d..d0d6593b42\n\nJonathan Gilbert (3):\n  git-gui: consolidate naming conventions\n  git-gui: update status bar to track operations\n  git-gui: revert untracked files by deleting them\n\n git-gui.sh          |  11 +-\n lib/blame.tcl       |  22 +-\n lib/checkout_op.tcl |  15 +-\n lib/chord.tcl       | 160 ++++++++++++++\n lib/index.tcl       | 523 +++++++++++++++++++++++++++++++++-----------\n lib/merge.tcl       |  14 +-\n lib/status_bar.tcl  | 228 ++++++++++++++++---\n 7 files changed, 797 insertions(+), 176 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v4:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n -:  ---------- > 2:  aa05a78d28 git-gui: update status bar to track operations\n 2:  23d4f5d829 ! 3:  d0d6593b42 git-gui: revert untracked files by deleting them\n     @@ -11,8 +11,8 @@\n          out of _close_updateindex to a responsibility of the caller, to permit\n          paths that don't directly unlock the index, and refactor the error\n          handling added in d4e890e5 so that callers can make flow control\n     -    decisions in the event of errors. Rework status_bar to explicitly handle\n     -    multiple overlapping operations, and update all call sites.\n     +    decisions in the event of errors. Update Tcl/Tk dependency from 8.4 to\n     +    8.6 in git-gui.sh.\n      \n          A new proc delete_files takes care of actually deleting the files in\n          batches, using the Tcler's Wiki recommended approach for keeping the UI\n     @@ -24,14 +24,6 @@\n          only after both operations are complete. The `SimpleChord` class,\n          based on TclOO (Tcl/Tk 8.6), is added in this commit.\n      \n     -    Since the checkout_index and delete_files calls are both asynchronous\n     -    and overlap, they clash in wanting to update the status bar. This commit\n     -    reworks the status bar so that when an operation wants to display\n     -    ongoing updates/progress, it explicitly starts an \"operation\", which is\n     -    tracked by its own object, and the status bar handles multiple\n     -    concurrent operations by merging their progress and concatenating their\n     -    text.\n     -\n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n       diff --git a/git-gui.sh b/git-gui.sh\n     @@ -48,73 +40,6 @@\n       } {\n       \tcatch {wm withdraw .}\n       \ttk_messageBox \\\n     -@@\n     - \tdo_explore\n     - }\n     - \n     -+# Clear \"Initializing...\" status\n     -+after idle {after 500 {$main_status show \"\"}}\n     -+\n     - # Local variables:\n     - # mode: tcl\n     - # indent-tabs-mode: t\n     -\n     - diff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\n     - --- a/lib/checkout_op.tcl\n     - +++ b/lib/checkout_op.tcl\n     -@@\n     - \tglobal HEAD\n     - \n     - \tset readtree_d {}\n     --\t$::main_status start \\\n     -+\tset status_bar_operation [$::main_status start \\\n     - \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n     --\t\t[mc \"files checked out\"]\n     -+\t\t[mc \"files checked out\"]]\n     - \n     - \tset fd [git_read --stderr read-tree \\\n     - \t\t-m \\\n     -@@\n     - \t\t$new_hash \\\n     - \t\t]\n     - \tfconfigure $fd -blocking 0 -translation binary\n     --\tfileevent $fd readable [cb _readtree_wait $fd]\n     -+\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n     - }\n     - \n     --method _readtree_wait {fd} {\n     -+method _readtree_wait {fd status_bar_operation} {\n     - \tglobal current_branch\n     - \n     - \tset buf [read $fd]\n     --\t$::main_status update_meter $buf\n     -+\t$status_bar_operation update_meter $buf\n     - \tappend readtree_d $buf\n     - \n     - \tfconfigure $fd -blocking 1\n     - \tif {![eof $fd]} {\n     - \t\tfconfigure $fd -blocking 0\n     -+\t\t$status_bar_operation stop\n     - \t\treturn\n     - \t}\n     - \n     - \tif {[catch {close $fd}]} {\n     - \t\tset err $readtree_d\n     - \t\tregsub {^fatal: } $err {} err\n     --\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n     -+\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n     - \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n     - \n     - $err\n     -@@\n     - \t\treturn\n     - \t}\n     - \n     --\t$::main_status stop\n     -+\t$status_bar_operation stop\n     - \t_after_readtree $this\n     - }\n     - \n      \n       diff --git a/lib/chord.tcl b/lib/chord.tcl\n       new file mode 100644\n     @@ -327,22 +252,18 @@\n      -\t\twm deiconify $w\n      -\t\ttkwait window $w\n      -\n     --\t\t$::main_status stop\n     -+# Returns true if the operation succeeded, false if a rescan has been initiated.\n     -+proc close_and_unlock_updateindex_rescan_on_error {fd after} {\n     +-\t\t$::main_status stop_all\n     ++proc close_and_unlock_index {fd after} {\n      +\tif {![catch {_close_updateindex $fd} err]} {\n       \t\tunlock_index\n      -\t\trescan $after 0\n      -\t\treturn\n      +\t\tuplevel #0 $after\n     -+\t\treturn 1\n      +\t} else {\n      +\t\trescan_on_error $err $after\n     -+\t\treturn 0\n       \t}\n      +}\n     - \n     --\t$::main_status stop\n     ++\n      +proc _close_updateindex {fd} {\n      +\tfconfigure $fd -blocking 1\n      +\tclose $fd\n     @@ -384,8 +305,8 @@\n      +\t\"\n      +\twm deiconify $w\n      +\ttkwait window $w\n     -+\n     -+\t$::main_status stop_all\n     + \n     + \t$::main_status stop_all\n       \tunlock_index\n      -\tuplevel #0 $after\n      +\trescan [concat $after [list ui_ready]] 0\n     @@ -393,82 +314,25 @@\n       \n       proc update_indexinfo {msg path_list after} {\n      @@\n     - \tset batch [expr {int($total_cnt * .01) + 1}]\n     - \tif {$batch > 25} {set batch 25}\n     - \n     --\t$::main_status start $msg [mc \"files\"]\n     -+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     - \tset fd [git_write update-index -z --index-info]\n     - \tfconfigure $fd \\\n     - \t\t-blocking 0 \\\n     -@@\n     - \t\t$path_list \\\n     - \t\t$total_cnt \\\n     - \t\t$batch \\\n     -+\t\t$status_bar_operation \\\n     - \t\t$after \\\n     - \t\t]\n     - }\n     - \n     --proc write_update_indexinfo {fd path_list total_cnt batch after} {\n     -+proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n     -+\tafter} {\n     - \tglobal update_index_cp\n     - \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n     + \t\t$status_bar_operation stop\n      -\t\t_close_updateindex $fd $after\n     -+\t\t$status_bar_operation stop\n     -+\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n     ++\t\tclose_and_unlock_index $fd $after\n       \t\treturn\n       \t}\n       \n      @@\n     - \t\tdisplay_file $path $new\n     - \t}\n     - \n     --\t$::main_status update $update_index_cp $total_cnt\n     -+\t$status_bar_operation update $update_index_cp $total_cnt\n     - }\n     - \n     - proc update_index {msg path_list after} {\n     -@@\n     - \tset batch [expr {int($total_cnt * .01) + 1}]\n     - \tif {$batch > 25} {set batch 25}\n     - \n     --\t$::main_status start $msg [mc \"files\"]\n     -+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     - \tset fd [git_write update-index --add --remove -z --stdin]\n     - \tfconfigure $fd \\\n     - \t\t-blocking 0 \\\n     -@@\n     - \t\t$path_list \\\n     - \t\t$total_cnt \\\n     - \t\t$batch \\\n     -+\t\t$status_bar_operation \\\n     - \t\t$after \\\n     - \t\t]\n     - }\n     - \n     --proc write_update_index {fd path_list total_cnt batch after} {\n     -+proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n     -+\tafter} {\n     - \tglobal update_index_cp\n     - \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n     + \t\t$status_bar_operation stop\n      -\t\t_close_updateindex $fd $after\n     -+\t\t$status_bar_operation stop\n     -+\t\tclose_and_unlock_updateindex_rescan_on_error $fd $after\n     ++\t\tclose_and_unlock_index $fd $after\n       \t\treturn\n       \t}\n       \n      @@\n     - \t\tdisplay_file $path $new\n     - \t}\n     - \n     --\t$::main_status update $update_index_cp $total_cnt\n     -+\t$status_bar_operation update $update_index_cp $total_cnt\n     + \t$status_bar_operation update $update_index_cp $total_cnt\n       }\n       \n      -proc checkout_index {msg path_list after} {\n     @@ -477,33 +341,31 @@\n       \n       \tif {![lock_index update]} return\n      @@\n     - \tset batch [expr {int($total_cnt * .01) + 1}]\n     - \tif {$batch > 25} {set batch 25}\n     - \n     --\t$::main_status start $msg [mc \"files\"]\n     -+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n     - \tset fd [git_write checkout-index \\\n     - \t\t--index \\\n     - \t\t--quiet \\\n     -@@\n     - \t\t$path_list \\\n     - \t\t$total_cnt \\\n       \t\t$batch \\\n     -+\t\t$status_bar_operation \\\n     + \t\t$status_bar_operation \\\n       \t\t$after \\\n      +\t\t$capture_error \\\n       \t\t]\n       }\n       \n     --proc write_checkout_index {fd path_list total_cnt batch after} {\n     -+proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n     + proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n     +-\tafter} {\n      +\tafter capture_error} {\n       \tglobal update_index_cp\n       \tglobal file_states current_diff_path\n       \n       \tif {$update_index_cp >= $total_cnt} {\n     + \t\t$status_bar_operation stop\n      -\t\t_close_updateindex $fd $after\n     -+\t\t$status_bar_operation stop\n     ++\n     ++\t\t# We do not unlock the index directly here because this\n     ++\t\t# operation expects to potentially run in parallel with file\n     ++\t\t# deletions scheduled by revert_helper. We're done with the\n     ++\t\t# update index, so we close it, but actually unlocking the index\n     ++\t\t# and dealing with potential errors is deferred to the chord\n     ++\t\t# body that runs when all async operations are completed.\n     ++\t\t#\n     ++\t\t# (See after_chord in revert_helper.)\n      +\n      +\t\tif {[catch {_close_updateindex $fd} err]} {\n      +\t\t\tuplevel #0 $capture_error [list $err]\n     @@ -514,15 +376,6 @@\n       \t\treturn\n       \t}\n       \n     -@@\n     - \t\t}\n     - \t}\n     - \n     --\t$::main_status update $update_index_cp $total_cnt\n     -+\t$status_bar_operation update $update_index_cp $total_cnt\n     - }\n     - \n     - proc unstage_helper {txt paths} {\n      @@\n       \tadd_helper [mc \"Adding all changed files\"] $paths\n       }\n     @@ -860,348 +713,3 @@\n       \t}\n       }\n       \n     -\n     - diff --git a/lib/merge.tcl b/lib/merge.tcl\n     - --- a/lib/merge.tcl\n     - +++ b/lib/merge.tcl\n     -@@\n     - \tif {[ask_popup $op_question] eq {yes}} {\n     - \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n     - \t\tfconfigure $fd -blocking 0 -translation binary\n     --\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n     --\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n     -+\t\tset status_bar_operation [$::main_status \\\n     -+\t\t\tstart \\\n     -+\t\t\t[mc \"Aborting\"] \\\n     -+\t\t\t[mc \"files reset\"]\n     -+\t\tfileevent $fd readable [namespace code [list \\\n     -+\t\t\t_reset_wait $fd $status_bar_operation]]\n     - \t} else {\n     - \t\tunlock_index\n     - \t}\n     - }\n     - \n     --proc _reset_wait {fd} {\n     -+proc _reset_wait {fd status_bar_operation} {\n     - \tglobal ui_comm\n     - \n     --\t$::main_status update_meter [read $fd]\n     -+\t$status_bar_operation update_meter [read $fd]\n     - \n     - \tfconfigure $fd -blocking 1\n     - \tif {[eof $fd]} {\n     - \t\tset fail [catch {close $fd} err]\n     --\t\t$::main_status stop\n     - \t\tunlock_index\n     -+\t\t$status_bar_operation stop\n     - \n     - \t\t$ui_comm delete 0.0 end\n     - \t\t$ui_comm edit modified false\n     -\n     - diff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\n     - --- a/lib/status_bar.tcl\n     - +++ b/lib/status_bar.tcl\n     -@@\n     - # git-gui status bar mega-widget\n     - # Copyright (C) 2007 Shawn Pearce\n     - \n     -+# The status_bar class manages the entire status bar. It is possible for\n     -+# multiple overlapping asynchronous operations to want to display status\n     -+# simultaneously. Each one receives a status_bar_operation when it calls the\n     -+# start method, and the status bar combines all active operations into the\n     -+# line of text it displays. Most of the time, there will be at most one\n     -+# ongoing operation.\n     -+#\n     -+# Note that the entire status bar can be either in single-line or two-line\n     -+# mode, depending on the constructor. Multiple active operations are only\n     -+# supported for single-line status bars.\n     -+\n     - class status_bar {\n     - \n     -+field allow_multiple ; # configured at construction\n     -+\n     - field w         ; # our own window path\n     - field w_l       ; # text widget we draw messages into\n     - field w_c       ; # canvas we draw a progress bar into\n     - field c_pack    ; # script to pack the canvas with\n     --field status  {}; # single line of text we show\n     --field prefix  {}; # text we format into status\n     --field units   {}; # unit of progress\n     --field meter   {}; # current core git progress meter (if active)\n     -+\n     -+field baseline_text   ; # text to show if there are no operations\n     -+field status_bar_text ; # combined text for all operations\n     -+\n     -+field operations ; # list of current ongoing operations\n     -+\n     -+# The status bar can display a progress bar, updated when consumers call the\n     -+# update method on their status_bar_operation. When there are multiple\n     -+# operations, the status bar shows the combined status of all operations.\n     -+#\n     -+# When an overlapping operation completes, the progress bar is going to\n     -+# abruptly have one fewer operation in the calculation, causing a discontinuity.\n     -+# Therefore, whenever an operation completes, if it is not the last operation,\n     -+# this counter is increased, and the progress bar is calculated as though there\n     -+# were still another operation at 100%. When the last operation completes, this\n     -+# is reset to 0.\n     -+field completed_operation_count\n     - \n     - constructor new {path} {\n     - \tglobal use_ttk NS\n     -@@\n     - \tset w_l $w.l\n     - \tset w_c $w.c\n     - \n     -+\t# Standard single-line status bar: Permit overlapping operations\n     -+\tset allow_multiple 1\n     -+\n     -+\tset baseline_text \"\"\n     -+\tset operations [list]\n     -+\tset completed_operation_count 0\n     -+\n     - \t${NS}::frame $w\n     - \tif {!$use_ttk} {\n     - \t\t$w configure -borderwidth 1 -relief sunken\n     - \t}\n     - \t${NS}::label $w_l \\\n     --\t\t-textvariable @status \\\n     -+\t\t-textvariable @status_bar_text \\\n     - \t\t-anchor w \\\n     - \t\t-justify left\n     - \tpack $w_l -side left\n     -@@\n     - \tset w_l $w.l\n     - \tset w_c $w.c\n     - \n     -+\t# Two-line status bar: Only one ongoing operation permitted.\n     -+\tset allow_multiple 0\n     -+\n     -+\tset baseline_text \"\"\n     -+\tset operations [list]\n     -+\n     - \t${NS}::frame $w\n     - \t${NS}::label $w_l \\\n     --\t\t-textvariable @status \\\n     -+\t\t-textvariable @status_bar_text \\\n     - \t\t-anchor w \\\n     - \t\t-justify left\n     - \tpack $w_l -anchor w -fill x\n     -@@\n     - \treturn $this\n     - }\n     - \n     --method start {msg uds} {\n     -+method ensure_canvas {} {\n     - \tif {[winfo exists $w_c]} {\n     - \t\t$w_c coords bar 0 0 0 20\n     - \t} else {\n     -@@\n     - \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n     - \t\teval $c_pack\n     - \t}\n     -+}\n     -+\n     -+method show {msg {test {}}} {\n     -+\tif {$test eq {} || $status eq $test} {\n     -+\t\t$this ensure_canvas\n     -+\t\tset baseline_text $msg\n     -+\t\t$this refresh\n     -+\t}\n     -+}\n     -+\n     -+method start {msg uds} {\n     -+\tset baseline_text \"\"\n     -+\n     -+\tif {!$allow_multiple && [llength $operations]} {\n     -+\t\treturn [lindex $operations 0]\n     -+\t}\n     -+\n     -+\t$this ensure_canvas\n     -+\n     -+\tset operation [status_bar_operation::new $this $msg $uds]\n     -+\n     -+\tlappend operations $operation\n     -+\n     -+\t$this refresh\n     -+\n     -+\treturn $operation\n     -+}\n     -+\n     -+method refresh {} {\n     -+\tset new_text \"\"\n     -+\n     -+\tset total [expr $completed_operation_count * 100]\n     -+\tset have $total\n     -+\n     -+\tforeach operation $operations {\n     -+\t\tif {$new_text != \"\"} {\n     -+\t\t\tappend new_text \" / \"\n     -+\t\t}\n     -+\n     -+\t\tappend new_text [$operation get_status]\n     -+\n     -+\t\tset total [expr $total + 100]\n     -+\t\tset have [expr $have + [$operation get_progress]]\n     -+\t}\n     -+\n     -+\tif {$new_text == \"\"} {\n     -+\t\tset new_text $baseline_text\n     -+\t}\n     -+\n     -+\tset status_bar_text $new_text\n     -+\n     -+\tset pixel_width 0\n     -+\tif {$have > 0} {\n     -+\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n     -+\t}\n     -+\n     -+\t$w_c coords bar 0 0 $pixel_width 20\n     -+}\n     -+\n     -+method stop {operation stop_msg} {\n     -+\tset idx [lsearch $operations $operation]\n     -+\n     -+\tif {$idx >= 0} {\n     -+\t\tset operations [lreplace $operations $idx $idx]\n     -+\t\tset completed_operation_count [expr \\\n     -+\t\t\t$completed_operation_count + 1]\n     -+\n     -+\t\tif {[llength operations] == 0} {\n     -+\t\t\tset completed_operation_count 0\n     -+\n     -+\t\t\tdestroy $w_c\n     -+\t\t\tif {$stop_msg ne {}} {\n     -+\t\t\t\tset baseline_text $stop_msg\n     -+\t\t\t}\n     -+\t\t}\n     -+\n     -+\t\t$this refresh\n     -+\t}\n     -+}\n     -+\n     -+method stop_all {{stop_msg {}}} {\n     -+\tset operations_copy $operations\n     -+\tset operations [list] # This makes the operation's call to stop a no-op.\n     -+\n     -+\tforeach $operation operations_copy {\n     -+\t\t$operation stop\n     -+\t}\n     -+\n     -+\tif {$stop_msg ne {}} {\n     -+\t\tset baseline_text $stop_msg\n     -+\t}\n     -+\n     -+\t$this refresh\n     -+}\n     -+\n     -+method _delete {current} {\n     -+\tif {$current eq $w} {\n     -+\t\tdelete_this\n     -+\t}\n     -+}\n     -+\n     -+}\n     -+\n     -+# The status_bar_operation class tracks a single consumer's ongoing status bar\n     -+# activity, with the context that there are a few situations where multiple\n     -+# overlapping asynchronous operations might want to display status information\n     -+# simultaneously. Instances of status_bar_operation are created by calling\n     -+# start on the status_bar, and when the caller is done with its stauts bar\n     -+# operation, it calls stop on the operation.\n     -+\n     -+class status_bar_operation {\n     -+\n     -+field status_bar; # reference back to the status_bar that owns this object\n     -+\n     -+field is_active;\n     -+\n     -+field status   {}; # single line of text we show\n     -+field progress {}; # current progress (0 to 100)\n     -+field prefix   {}; # text we format into status\n     -+field units    {}; # unit of progress\n     -+field meter    {}; # current core git progress meter (if active)\n     -+\n     -+constructor new {owner msg uds} {\n     -+\tset status_bar $owner\n     - \n     - \tset status $msg\n     -+\tset progress 0\n     - \tset prefix $msg\n     - \tset units  $uds\n     - \tset meter  {}\n     -+\n     -+\tset is_active 1\n     -+\n     -+\treturn $this\n     - }\n     - \n     -+method get_is_active {} { return $is_active }\n     -+method get_status {} { return $status }\n     -+method get_progress {} { return $progress }\n     -+\n     - method update {have total} {\n     --\tset pdone 0\n     --\tset cdone 0\n     -+\tif {!$is_active} { return }\n     -+\n     -+\tset progress 0\n     -+\n     - \tif {$total > 0} {\n     --\t\tset pdone [expr {100 * $have / $total}]\n     --\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n     -+\t\tset progress [expr {100 * $have / $total}]\n     - \t}\n     - \n     - \tset prec [string length [format %i $total]]\n     -+\n     - \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n     - \t\t$prefix \\\n     - \t\t$prec $have \\\n     - \t\t$prec $total \\\n     --\t\t$units $pdone]\n     --\t$w_c coords bar 0 0 $cdone 20\n     -+\t\t$units $progress]\n     -+\n     -+\t$status_bar refresh\n     - }\n     - \n     - method update_meter {buf} {\n     -+\tif {!$is_active} { return }\n     -+\n     - \tappend meter $buf\n     - \tset r [string last \"\\r\" $meter]\n     - \tif {$r == -1} {\n     -@@\n     - \t}\n     - }\n     - \n     --method stop {{msg {}}} {\n     --\tdestroy $w_c\n     --\tif {$msg ne {}} {\n     --\t\tset status $msg\n     -+method stop {{stop_msg {}}} {\n     -+\tif {$is_active} {\n     -+\t\tset is_active 0\n     -+\t\t$status_bar stop $this $stop_msg\n     - \t}\n     - }\n     - \n     - method show {msg {test {}}} {\n     -+\tif {!$is_active} { return }\n     -+\n     - \tif {$test eq {} || $status eq $test} {\n     - \t\tset status $msg\n     -+\t\t$status_bar refresh\n     - \t}\n     - }\n     - \n     --method _delete {current} {\n     --\tif {$current eq $w} {\n     --\t\tdelete_this\n     --\t}\n     -+method _delete {} {\n     -+\tstop\n     -+\tdelete_this\n     - }\n     - \n     - }\n\n-- \ngitgitgadget\n"},{"id":"386955","messageId":"aa05a78d285dd9c5f4897b03467f1d43d7a5ad83.1574627876.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v5.git.1574627876.gitgitgadget@gmail.com","subject":"[PATCH v5 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-24T20:37:54Z","receivedAt":"2019-11-24T20:38:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the status bar to track updates as individual \"operations\" that\ncan overlap. Update all call sites to interact with the new status bar\nmechanism. Update initialization to explicitly clear status text,\nsince otherwise it may persist across future operations.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh          |   7 +-\n lib/blame.tcl       |  22 +++--\n lib/checkout_op.tcl |  15 +--\n lib/index.tcl       |  31 +++---\n lib/merge.tcl       |  14 ++-\n lib/status_bar.tcl  | 228 +++++++++++++++++++++++++++++++++++++++-----\n 6 files changed, 260 insertions(+), 57 deletions(-)\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex 0d21f5688b..db02e399e7 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -1797,10 +1797,10 @@ proc ui_status {msg} {\n \t}\n }\n \n-proc ui_ready {{test {}}} {\n+proc ui_ready {} {\n \tglobal main_status\n \tif {[info exists main_status]} {\n-\t\t$main_status show [mc \"Ready.\"] $test\n+\t\t$main_status show [mc \"Ready.\"]\n \t}\n }\n \n@@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n \tdo_explore\n }\n \n+# Clear \"Initializing...\" status\n+after 500 {$main_status show \"\"}\n+\n # Local variables:\n # mode: tcl\n # indent-tabs-mode: t\ndiff --git a/lib/blame.tcl b/lib/blame.tcl\nindex a1aeb8b96e..888f98bab2 100644\n--- a/lib/blame.tcl\n+++ b/lib/blame.tcl\n@@ -24,6 +24,7 @@ field w_cviewer  ; # pane showing commit message\n field finder     ; # find mini-dialog frame\n field gotoline   ; # line goto mini-dialog frame\n field status     ; # status mega-widget instance\n+field status_operation ; # status operation\n field old_height ; # last known height of $w.file_pane\n \n \n@@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {\n \tpack $w_cviewer -expand 1 -fill both\n \n \tset status [::status_bar::new $w.status]\n+\tset status_operation {}\n \n \tmenu $w.ctxm -tearoff 0\n \t$w.ctxm add command \\\n@@ -602,16 +604,21 @@ method _exec_blame {cur_w cur_d options cur_s} {\n \t} else {\n \t\tlappend options $commit\n \t}\n+\n+\t# We may recurse in from another call to _exec_blame and already have\n+\t# a status operation.\n+\tif {$status_operation == {}} {\n+\t\tset status_operation [$status start \\\n+\t\t\t$cur_s \\\n+\t\t\t[mc \"lines annotated\"]]\n+\t}\n+\n \tlappend options -- $path\n \tset fd [eval git_read --nice blame $options]\n \tfconfigure $fd -blocking 0 -translation lf -encoding utf-8\n \tfileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]\n \tset current_fd $fd\n \tset blame_lines 0\n-\n-\t$status start \\\n-\t\t$cur_s \\\n-\t\t[mc \"lines annotated\"]\n }\n \n method _read_blame {fd cur_w cur_d} {\n@@ -806,10 +813,11 @@ method _read_blame {fd cur_w cur_d} {\n \t\t\t\t[mc \"Loading original location annotations...\"]\n \t\t} else {\n \t\t\tset current_fd {}\n-\t\t\t$status stop [mc \"Annotation complete.\"]\n+\t\t\t$status_operation stop [mc \"Annotation complete.\"]\n+\t\t\tset status_operation {}\n \t\t}\n \t} else {\n-\t\t$status update $blame_lines $total_lines\n+\t\t$status_operation update $blame_lines $total_lines\n \t}\n } ifdeleted { catch {close $fd} }\n \n@@ -1124,7 +1132,7 @@ method _blameparent {} {\n \t\t\tset diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]\n \t\t}\n \t\tif {[catch {set fd [eval git_read $diffcmd]} err]} {\n-\t\t\t$status stop [mc \"Unable to display parent\"]\n+\t\t\t$status_operation stop [mc \"Unable to display parent\"]\n \t\t\terror_popup [strcat [mc \"Error loading diff:\"] \"\\n\\n$err\"]\n \t\t\treturn\n \t\t}\ndiff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\nindex a5228297db..21ea768d80 100644\n--- a/lib/checkout_op.tcl\n+++ b/lib/checkout_op.tcl\n@@ -341,9 +341,9 @@ method _readtree {} {\n \tglobal HEAD\n \n \tset readtree_d {}\n-\t$::main_status start \\\n+\tset status_bar_operation [$::main_status start \\\n \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n-\t\t[mc \"files checked out\"]\n+\t\t[mc \"files checked out\"]]\n \n \tset fd [git_read --stderr read-tree \\\n \t\t-m \\\n@@ -354,26 +354,27 @@ method _readtree {} {\n \t\t$new_hash \\\n \t\t]\n \tfconfigure $fd -blocking 0 -translation binary\n-\tfileevent $fd readable [cb _readtree_wait $fd]\n+\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n }\n \n-method _readtree_wait {fd} {\n+method _readtree_wait {fd status_bar_operation} {\n \tglobal current_branch\n \n \tset buf [read $fd]\n-\t$::main_status update_meter $buf\n+\t$status_bar_operation update_meter $buf\n \tappend readtree_d $buf\n \n \tfconfigure $fd -blocking 1\n \tif {![eof $fd]} {\n \t\tfconfigure $fd -blocking 0\n+\t\t$status_bar_operation stop\n \t\treturn\n \t}\n \n \tif {[catch {close $fd}]} {\n \t\tset err $readtree_d\n \t\tregsub {^fatal: } $err {} err\n-\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n+\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n \n $err\n@@ -384,7 +385,7 @@ $err\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$status_bar_operation stop\n \t_after_readtree $this\n }\n \ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..62f4773ef4 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -45,13 +45,13 @@ proc _close_updateindex {fd after} {\n \t\twm deiconify $w\n \t\ttkwait window $w\n \n-\t\t$::main_status stop\n+\t\t$::main_status stop_all\n \t\tunlock_index\n \t\trescan $after 0\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$::main_status stop_all\n \tunlock_index\n \tuplevel #0 $after\n }\n@@ -67,7 +67,7 @@ proc update_indexinfo {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index -z --index-info]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -81,15 +81,18 @@ proc update_indexinfo {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd path_list total_cnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -119,7 +122,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc update_index {msg path_list after} {\n@@ -133,7 +136,7 @@ proc update_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index --add --remove -z --stdin]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -147,15 +150,18 @@ proc update_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd path_list total_cnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -190,7 +196,7 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc checkout_index {msg path_list after} {\n@@ -204,7 +210,7 @@ proc checkout_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write checkout-index \\\n \t\t--index \\\n \t\t--quiet \\\n@@ -224,15 +230,18 @@ proc checkout_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd path_list total_cnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -253,7 +262,7 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\ndiff --git a/lib/merge.tcl b/lib/merge.tcl\nindex 9f253db5b3..8df8ffae55 100644\n--- a/lib/merge.tcl\n+++ b/lib/merge.tcl\n@@ -241,23 +241,27 @@ Continue with resetting the current changes?\"]\n \tif {[ask_popup $op_question] eq {yes}} {\n \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n \t\tfconfigure $fd -blocking 0 -translation binary\n-\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n-\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n+\t\tset status_bar_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Aborting\"] \\\n+\t\t\t[mc \"files reset\"]\n+\t\tfileevent $fd readable [namespace code [list \\\n+\t\t\t_reset_wait $fd $status_bar_operation]]\n \t} else {\n \t\tunlock_index\n \t}\n }\n \n-proc _reset_wait {fd} {\n+proc _reset_wait {fd status_bar_operation} {\n \tglobal ui_comm\n \n-\t$::main_status update_meter [read $fd]\n+\t$status_bar_operation update_meter [read $fd]\n \n \tfconfigure $fd -blocking 1\n \tif {[eof $fd]} {\n \t\tset fail [catch {close $fd} err]\n-\t\t$::main_status stop\n \t\tunlock_index\n+\t\t$status_bar_operation stop\n \n \t\t$ui_comm delete 0.0 end\n \t\t$ui_comm edit modified false\ndiff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\nindex 02111a1742..c4bff98856 100644\n--- a/lib/status_bar.tcl\n+++ b/lib/status_bar.tcl\n@@ -1,16 +1,42 @@\n # git-gui status bar mega-widget\n # Copyright (C) 2007 Shawn Pearce\n \n+# The status_bar class manages the entire status bar. It is possible for\n+# multiple overlapping asynchronous operations to want to display status\n+# simultaneously. Each one receives a status_bar_operation when it calls the\n+# start method, and the status bar combines all active operations into the\n+# line of text it displays. Most of the time, there will be at most one\n+# ongoing operation.\n+#\n+# Note that the entire status bar can be either in single-line or two-line\n+# mode, depending on the constructor. Multiple active operations are only\n+# supported for single-line status bars.\n+\n class status_bar {\n \n+field allow_multiple ; # configured at construction\n+\n field w         ; # our own window path\n field w_l       ; # text widget we draw messages into\n field w_c       ; # canvas we draw a progress bar into\n field c_pack    ; # script to pack the canvas with\n-field status  {}; # single line of text we show\n-field prefix  {}; # text we format into status\n-field units   {}; # unit of progress\n-field meter   {}; # current core git progress meter (if active)\n+\n+field baseline_text   ; # text to show if there are no operations\n+field status_bar_text ; # combined text for all operations\n+\n+field operations ; # list of current ongoing operations\n+\n+# The status bar can display a progress bar, updated when consumers call the\n+# update method on their status_bar_operation. When there are multiple\n+# operations, the status bar shows the combined status of all operations.\n+#\n+# When an overlapping operation completes, the progress bar is going to\n+# abruptly have one fewer operation in the calculation, causing a discontinuity.\n+# Therefore, whenever an operation completes, if it is not the last operation,\n+# this counter is increased, and the progress bar is calculated as though there\n+# were still another operation at 100%. When the last operation completes, this\n+# is reset to 0.\n+field completed_operation_count\n \n constructor new {path} {\n \tglobal use_ttk NS\n@@ -18,12 +44,19 @@ constructor new {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Standard single-line status bar: Permit overlapping operations\n+\tset allow_multiple 1\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \tif {!$use_ttk} {\n \t\t$w configure -borderwidth 1 -relief sunken\n \t}\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -side left\n@@ -44,9 +77,15 @@ constructor two_line {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Two-line status bar: Only one ongoing operation permitted.\n+\tset allow_multiple 0\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\n \t${NS}::frame $w\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -anchor w -fill x\n@@ -56,7 +95,7 @@ constructor two_line {path} {\n \treturn $this\n }\n \n-method start {msg uds} {\n+method ensure_canvas {} {\n \tif {[winfo exists $w_c]} {\n \t\t$w_c coords bar 0 0 0 20\n \t} else {\n@@ -68,31 +107,170 @@ method start {msg uds} {\n \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n \t\teval $c_pack\n \t}\n+}\n+\n+method show {msg} {\n+\t$this ensure_canvas\n+\tset baseline_text $msg\n+\t$this refresh\n+}\n+\n+method start {msg uds} {\n+\tset baseline_text \"\"\n+\n+\tif {!$allow_multiple && [llength $operations]} {\n+\t\treturn [lindex $operations 0]\n+\t}\n+\n+\t$this ensure_canvas\n+\n+\tset operation [status_bar_operation::new $this $msg $uds]\n+\n+\tlappend operations $operation\n+\n+\t$this refresh\n+\n+\treturn $operation\n+}\n+\n+method refresh {} {\n+\tset new_text \"\"\n+\n+\tset total [expr $completed_operation_count * 100]\n+\tset have $total\n+\n+\tforeach operation $operations {\n+\t\tif {$new_text != \"\"} {\n+\t\t\tappend new_text \" / \"\n+\t\t}\n+\n+\t\tappend new_text [$operation get_status]\n+\n+\t\tset total [expr $total + 100]\n+\t\tset have [expr $have + [$operation get_progress]]\n+\t}\n+\n+\tif {$new_text == \"\"} {\n+\t\tset new_text $baseline_text\n+\t}\n+\n+\tset status_bar_text $new_text\n+\n+\tif {[winfo exists $w_c]} {\n+\t\tset pixel_width 0\n+\t\tif {$have > 0} {\n+\t\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n+\t\t}\n+\n+\t\t$w_c coords bar 0 0 $pixel_width 20\n+\t}\n+}\n+\n+method stop {operation stop_msg} {\n+\tset idx [lsearch $operations $operation]\n+\n+\tif {$idx >= 0} {\n+\t\tset operations [lreplace $operations $idx $idx]\n+\t\tset completed_operation_count [expr \\\n+\t\t\t$completed_operation_count + 1]\n+\n+\t\tif {[llength $operations] == 0} {\n+\t\t\tset completed_operation_count 0\n+\n+\t\t\tdestroy $w_c\n+\t\t\tif {$stop_msg ne {}} {\n+\t\t\t\tset baseline_text $stop_msg\n+\t\t\t}\n+\t\t}\n+\n+\t\t$this refresh\n+\t}\n+}\n+\n+method stop_all {{stop_msg {}}} {\n+\t# This makes the operation's call to stop a no-op.\n+\tset operations_copy $operations\n+\tset operations [list]\n+\n+\tforeach operation $operations_copy {\n+\t\t$operation stop\n+\t}\n+\n+\tif {$stop_msg ne {}} {\n+\t\tset baseline_text $stop_msg\n+\t}\n+\n+\t$this refresh\n+}\n+\n+method _delete {current} {\n+\tif {$current eq $w} {\n+\t\tdelete_this\n+\t}\n+}\n+\n+}\n+\n+# The status_bar_operation class tracks a single consumer's ongoing status bar\n+# activity, with the context that there are a few situations where multiple\n+# overlapping asynchronous operations might want to display status information\n+# simultaneously. Instances of status_bar_operation are created by calling\n+# start on the status_bar, and when the caller is done with its stauts bar\n+# operation, it calls stop on the operation.\n+\n+class status_bar_operation {\n+\n+field status_bar; # reference back to the status_bar that owns this object\n+\n+field is_active;\n+\n+field status   {}; # single line of text we show\n+field progress {}; # current progress (0 to 100)\n+field prefix   {}; # text we format into status\n+field units    {}; # unit of progress\n+field meter    {}; # current core git progress meter (if active)\n+\n+constructor new {owner msg uds} {\n+\tset status_bar $owner\n \n \tset status $msg\n+\tset progress 0\n \tset prefix $msg\n \tset units  $uds\n \tset meter  {}\n+\n+\tset is_active 1\n+\n+\treturn $this\n }\n \n+method get_is_active {} { return $is_active }\n+method get_status {} { return $status }\n+method get_progress {} { return $progress }\n+\n method update {have total} {\n-\tset pdone 0\n-\tset cdone 0\n+\tif {!$is_active} { return }\n+\n+\tset progress 0\n+\n \tif {$total > 0} {\n-\t\tset pdone [expr {100 * $have / $total}]\n-\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n+\t\tset progress [expr {100 * $have / $total}]\n \t}\n \n \tset prec [string length [format %i $total]]\n+\n \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n \t\t$prefix \\\n \t\t$prec $have \\\n \t\t$prec $total \\\n-\t\t$units $pdone]\n-\t$w_c coords bar 0 0 $cdone 20\n+\t\t$units $progress]\n+\n+\t$status_bar refresh\n }\n \n method update_meter {buf} {\n+\tif {!$is_active} { return }\n+\n \tappend meter $buf\n \tset r [string last \"\\r\" $meter]\n \tif {$r == -1} {\n@@ -109,23 +287,23 @@ method update_meter {buf} {\n \t}\n }\n \n-method stop {{msg {}}} {\n-\tdestroy $w_c\n-\tif {$msg ne {}} {\n-\t\tset status $msg\n+method stop {{stop_msg {}}} {\n+\tif {$is_active} {\n+\t\tset is_active 0\n+\t\t$status_bar stop $this $stop_msg\n \t}\n }\n \n-method show {msg {test {}}} {\n-\tif {$test eq {} || $status eq $test} {\n-\t\tset status $msg\n-\t}\n+method show {msg} {\n+\tif {!$is_active} { return }\n+\n+\tset status $msg\n+\t$status_bar refresh\n }\n \n-method _delete {current} {\n-\tif {$current eq $w} {\n-\t\tdelete_this\n-\t}\n+method _delete {} {\n+\tstop\n+\tdelete_this\n }\n \n }\n-- \ngitgitgadget\n\n"},{"id":"386956","messageId":"d0d6593b421609445e8f2c794e1d3ca46ebed068.1574627876.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v5.git.1574627876.gitgitgadget@gmail.com","subject":"[PATCH v5 3/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-24T20:37:55Z","receivedAt":"2019-11-24T20:38:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper proc to check for untracked files as well as\nchanges, and then handle changes to be reverted and untracked files with\nindependent blocks of code. Prompt the user independently for untracked\nfiles, since the underlying action is fundamentally different (rm -f).\nIf after deleting untracked files, the directory containing them becomes\nempty, then remove the directory as well. Migrate unlocking of the index\nout of _close_updateindex to a responsibility of the caller, to permit\npaths that don't directly unlock the index, and refactor the error\nhandling added in d4e890e5 so that callers can make flow control\ndecisions in the event of errors. Update Tcl/Tk dependency from 8.4 to\n8.6 in git-gui.sh.\n\nA new proc delete_files takes care of actually deleting the files in\nbatches, using the Tcler's Wiki recommended approach for keeping the UI\nresponsive.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand could potentially complete in any order, a \"chord\" is used to\ncoordinate unlocking the index and returning the UI to a usable state\nonly after both operations are complete. The `SimpleChord` class,\nbased on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh    |   4 +-\n lib/chord.tcl | 160 +++++++++++++++++++\n lib/index.tcl | 422 ++++++++++++++++++++++++++++++++++++++++----------\n 3 files changed, 502 insertions(+), 84 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex db02e399e7..ad87703b2d 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n ##\n ## Tcl/Tk sanity check\n \n-if {[catch {package require Tcl 8.4} err]\n- || [catch {package require Tk  8.4} err]\n+if {[catch {package require Tcl 8.6} err]\n+ || [catch {package require Tk  8.6} err]\n } {\n \tcatch {wm withdraw .}\n \ttk_messageBox \\\ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..275a6cd4a1\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,160 @@\n+# Simple Chord for Tcl\n+#\n+# A \"chord\" is a method with more than one entrypoint and only one body, such\n+# that the body runs only once all the entrypoints have been called by\n+# different asynchronous tasks. In this implementation, the chord is defined\n+# dynamically for each invocation. A SimpleChord object is created, supplying\n+# body script to be run when the chord is completed, and then one or more notes\n+# are added to the chord. Each note can be called like a proc, and returns\n+# immediately if the chord isn't yet complete. When the last remaining note is\n+# called, the body runs before the note returns.\n+#\n+# The SimpleChord class has a constructor that takes the body script, and a\n+# method add_note that returns a note object. Since the body script does not\n+# run in the context of the procedure that defined it, a mechanism is provided\n+# for injecting variables into the chord for use by the body script. The\n+# activation of a note is idempotent; multiple calls have the same effect as\n+# a simple call.\n+#\n+# If you are invoking asynchronous operations with chord notes as completion\n+# callbacks, and there is a possibility that earlier operations could complete\n+# before later ones are started, it is a good practice to create a \"common\"\n+# note on the chord that prevents it from being complete until you're certain\n+# you've added all the notes you need.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+oo::class create SimpleChord {\n+\tvariable notes body is_completed\n+\n+\t# Constructor:\n+\t#   set chord [SimpleChord new {body}]\n+\t#     Creates a new chord object with the specified body script. The\n+\t#     body script is evaluated at most once, when a note is activated\n+\t#     and the chord has no other non-activated notes.\n+\tconstructor {body} {\n+\t\tset notes [list]\n+\t\tmy eval [list set body $body]\n+\t\tset is_completed 0\n+\t}\n+\n+\t# Method:\n+\t#   $chord eval {script}\n+\t#     Runs the specified script in the same context (namespace) in which\n+\t#     the chord body will be evaluated. This can be used to set variable\n+\t#     values for the chord body to use.\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\t# Method:\n+\t#   set note [$chord add_note]\n+\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n+\t#     error if the chord is already completed, otherwise the chord is\n+\t#     updated so that the new note must also be activated before the\n+\t#     body is evaluated.\n+\tmethod add_note {} {\n+\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\t# This method is for internal use only and is intentionally undocumented.\n+\tmethod notify_note_activation {} {\n+\t\tif {!$is_completed} {\n+\t\t\tforeach note $notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset is_completed 1\n+\n+\t\t\tnamespace eval [self] $body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+oo::class create ChordNote {\n+\tvariable chord is_activated\n+\n+\t# Constructor:\n+\t#   Instances of ChordNote are created internally by calling add_note on\n+\t#   SimpleChord objects.\n+\tconstructor {chord} {\n+\t\tmy eval set chord $chord\n+\t\tset is_activated 0\n+\t}\n+\n+\t# Method:\n+\t#   [$note is_activated]\n+\t#     Returns true if this note has already been activated.\n+\tmethod is_activated {} {\n+\t\treturn $is_activated\n+\t}\n+\n+\t# Method:\n+\t#   $note\n+\t#     Activates the note, if it has not already been activated, and\n+\t#     completes the chord if there are no other notes awaiting\n+\t#     activation. Subsequent calls will have no further effect.\n+\t#\n+\t# NB: In TclOO, if an object is invoked like a method without supplying\n+\t#     any method name, then this internal method `unknown` is what\n+\t#     actually runs (with no parameters). It is used in the ChordNote\n+\t#     class for the purpose of allowing the note object to be called as\n+\t#     a function (see example above). (The `unknown` method can also be\n+\t#     used to support dynamic dispatch, but must take parameters to\n+\t#     identify the \"unknown\" method to be invoked. In this form, this\n+\t#     proc serves only to make instances behave directly like methods.)\n+\tmethod unknown {} {\n+\t\tif {!$is_activated} {\n+\t\t\tset is_activated 1\n+\t\t\t$chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 62f4773ef4..1254145634 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,53 +7,60 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n-\tglobal use_ttk NS\n-\tfconfigure $fd -blocking 1\n-\tif {[catch {close $fd} err]} {\n-\t\tset w .indexfried\n-\t\tDialog $w\n-\t\twm withdraw $w\n-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n-\t\t\t-width [string length $s] -relief flat \\\n-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n-\t\t\t-background [get_bg_color $w]\n-\t\t$w.msg tag configure bold -font font_uibold -justify center\n-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n-\t\t$w.msg insert end $s bold \\n\\n$err {}\n-\t\t$w.msg configure -state disabled\n-\n-\t\t${NS}::button $w.continue \\\n-\t\t\t-text [mc \"Continue\"] \\\n-\t\t\t-command [list destroy $w]\n-\t\t${NS}::button $w.unlock \\\n-\t\t\t-text [mc \"Unlock Index\"] \\\n-\t\t\t-command \"destroy $w; _delete_indexlock\"\n-\t\tgrid $w.msg - $w.vs -sticky news\n-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n-\t\tgrid columnconfigure $w 0 -weight 1\n-\t\tgrid rowconfigure $w 0 -weight 1\n-\n-\t\twm protocol $w WM_DELETE_WINDOW update\n-\t\tbind $w.continue <Visibility> \"\n-\t\t\tgrab $w\n-\t\t\tfocus %W\n-\t\t\"\n-\t\twm deiconify $w\n-\t\ttkwait window $w\n-\n-\t\t$::main_status stop_all\n+proc close_and_unlock_index {fd after} {\n+\tif {![catch {_close_updateindex $fd} err]} {\n \t\tunlock_index\n-\t\trescan $after 0\n-\t\treturn\n+\t\tuplevel #0 $after\n+\t} else {\n+\t\trescan_on_error $err $after\n \t}\n+}\n+\n+proc _close_updateindex {fd} {\n+\tfconfigure $fd -blocking 1\n+\tclose $fd\n+}\n+\n+proc rescan_on_error {err {after {}}} {\n+\tglobal use_ttk NS\n+\n+\tset w .indexfried\n+\tDialog $w\n+\twm withdraw $w\n+\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n+\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n+\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n+\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n+\t\t-width [string length $s] -relief flat \\\n+\t\t-borderwidth 0 -highlightthickness 0 \\\n+\t\t-background [get_bg_color $w]\n+\t$w.msg tag configure bold -font font_uibold -justify center\n+\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n+\t$w.msg insert end $s bold \\n\\n$err {}\n+\t$w.msg configure -state disabled\n+\n+\t${NS}::button $w.continue \\\n+\t\t-text [mc \"Continue\"] \\\n+\t\t-command [list destroy $w]\n+\t${NS}::button $w.unlock \\\n+\t\t-text [mc \"Unlock Index\"] \\\n+\t\t-command \"destroy $w; _delete_indexlock\"\n+\tgrid $w.msg - $w.vs -sticky news\n+\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n+\tgrid columnconfigure $w 0 -weight 1\n+\tgrid rowconfigure $w 0 -weight 1\n+\n+\twm protocol $w WM_DELETE_WINDOW update\n+\tbind $w.continue <Visibility> \"\n+\t\tgrab $w\n+\t\tfocus %W\n+\t\"\n+\twm deiconify $w\n+\ttkwait window $w\n \n \t$::main_status stop_all\n \tunlock_index\n-\tuplevel #0 $after\n+\trescan [concat $after [list ui_ready]] 0\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -93,7 +100,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -162,7 +169,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -199,7 +206,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg path_list after} {\n+proc checkout_index {msg path_list after capture_error} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n@@ -232,17 +239,33 @@ proc checkout_index {msg path_list after} {\n \t\t$batch \\\n \t\t$status_bar_operation \\\n \t\t$after \\\n+\t\t$capture_error \\\n \t\t]\n }\n \n proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n-\tafter} {\n+\tafter capture_error} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\n+\t\t# We do not unlock the index directly here because this\n+\t\t# operation expects to potentially run in parallel with file\n+\t\t# deletions scheduled by revert_helper. We're done with the\n+\t\t# update index, so we close it, but actually unlocking the index\n+\t\t# and dealing with potential errors is deferred to the chord\n+\t\t# body that runs when all async operations are completed.\n+\t\t#\n+\t\t# (See after_chord in revert_helper.)\n+\n+\t\tif {[catch {_close_updateindex $fd} err]} {\n+\t\t\tuplevel #0 $capture_error [list $err]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\n \t\treturn\n \t}\n \n@@ -397,66 +420,301 @@ proc do_add_all {} {\n \tadd_helper [mc \"Adding all changed files\"] $paths\n }\n \n+# Copied from TclLib package \"lambda\".\n+proc lambda {arguments body args} {\n+\treturn [list ::apply [list $arguments $body] {*}$args]\n+}\n+\n proc revert_helper {txt paths} {\n \tglobal file_states current_diff_path\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\t#\n+\t# The asynchronous operations are each indicated below by a comment\n+\t# before the code block that starts the async operation.\n+\tset after_chord [SimpleChord new {\n+\t\tif {[string trim $err] != \"\"} {\n+\t\t\trescan_on_error $err\n+\t\t} else {\n+\t\t\tunlock_index\n+\t\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\t\tui_ready\n+\t\t}\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# This function captures an error for processing when after_chord is\n+\t# completed. (The chord is curried into the lambda function.)\n+\tset capture_error [lambda \\\n+\t\t{chord error} \\\n+\t\t{ $chord eval [list set err $error] } \\\n+\t\t$after_chord]\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n-\tset after {}\n+\tset untracked_list [list]\n+\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n+\n+\t# Asynchronous operation: revert changes by checking them out afresh\n+\t# from the index.\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note] \\\n+\t\t\t\t$capture_error\n+\t\t}\n \t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t# Asynchronous operation: Deletion of untracked files.\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n \n-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\tset status_bar_operation [$::main_status \\\n+\t\tstart \\\n+\t\t[mc \"Deleting\"] \\\n+\t\t[mc \"files\"]]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$status_bar_operation \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size \\\n+\tstatus_bar_operation after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$status_bar_operation update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$status_bar_operation \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$status_bar_operation stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0)\n+\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"387079","messageId":"c3a21056-96c9-988d-2559-deb9ebedf642@iee.email","threadId":"52154","inReplyTo":"CAPSOpYt71DGzkQxQDaMm88hR5bO8npKEY7tBPTypZ+aE1yp6gA@mail.gmail.com","subject":"Re: [PATCH v2 2/2] git-gui: revert untracked files by deleting them","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2019-11-26T11:22:11Z","receivedAt":"2019-11-26T11:22:17Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 12/11/2019 16:29, Jonathan Gilbert wrote:\n> On Tue, Nov 12, 2019 at 4:45 AM Philip Oakley <philipoakley@iee.email> wrote:\n>> On 12/11/2019 04:49, Jonathan Gilbert wrote:\n>>> On Mon, Nov 11, 2019 at 4:59 PM Philip Oakley <philipoakley@iee.email> wrote:\n>>>> sounds like \"Currying\" a function but with the parameters taken in any\n>>>> order, though, in a sense, perhaps not generating intermediate functions...\n>>> It's like currying if you could pass g(x) = f(x, y) to one block of\n>>> code and h(y) = f(x, y) to another block of code, so that each of g\n>>> and h are each like curried versions of f that \"bake in\" one of the\n>>> arguments, without having to know which one will get called first. :-)\n>>>\n>>> Jonathan Gilbert\n>> So that would be called \"Chording\"...\n>> (Is there a 'proper' technical term for that approach?)\n> Not an entirely implausible term :-) The only other implementation\n> I've ever seen was Microsoft's \"Polyphonic C#\", which got rolled into\n> C-omega. I'm pretty sure, though, that it was never referred to as\n> something you _do to_ a function, but rather as a _different type_ of\n> function -- as in, the function hasn't been \"chorded\", it \"is a\n> chord\". Very little literature one way or the other though, and this\n> is the first actual, live use case for the structure I've encountered\n> in my years of programming :-)\n>\nA little bit of late follow up ;-)\n\nThe basic ideas that are embedded in \"chording\" would appear to be the \nsame as those used in Data Flow Diagrams and the older attempts at data \nflow based machines such as the Transputer and it's message passing, and \nout of order execution machines. See \nhttps://en.wikipedia.org/wiki/Dataflow_architecture etc.\n\nIt just looks like it's now moved to the compiler, or JIT (just-in-time) \ncompilation, which appears to be the same thing with different branding!\n\nPhilip\n"},{"id":"387249","messageId":"20191127215503.x2lt2b3nce7q4yj2@yadavpratyush.com","threadId":"52154","inReplyTo":"aa05a78d285dd9c5f4897b03467f1d43d7a5ad83.1574627876.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 2/3] git-gui: update status bar to track operations","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-27T21:55:03Z","receivedAt":"2019-11-27T21:55:13Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the re-roll.\n\nOn 24/11/19 08:37PM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the status bar to track updates as individual \"operations\" that\n> can overlap. Update all call sites to interact with the new status bar\n> mechanism. Update initialization to explicitly clear status text,\n> since otherwise it may persist across future operations.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  git-gui.sh          |   7 +-\n>  lib/blame.tcl       |  22 +++--\n>  lib/checkout_op.tcl |  15 +--\n>  lib/index.tcl       |  31 +++---\n>  lib/merge.tcl       |  14 ++-\n>  lib/status_bar.tcl  | 228 +++++++++++++++++++++++++++++++++++++++-----\n>  6 files changed, 260 insertions(+), 57 deletions(-)\n> \n> diff --git a/git-gui.sh b/git-gui.sh\n> index 0d21f5688b..db02e399e7 100755\n> --- a/git-gui.sh\n> +++ b/git-gui.sh\n> @@ -1797,10 +1797,10 @@ proc ui_status {msg} {\n>  \t}\n>  }\n>  \n> -proc ui_ready {{test {}}} {\n> +proc ui_ready {} {\n\nThis is not quite correct. There is one user of 'ui_ready' that uses \n'test'. It is in git-gui.sh:2211. It is used when starting gitk. This \nchange breaks that call. 10 seconds after opening gitk via the \n\"Visualise master's history\" option, I get the following error:\n\n  wrong # args: should be \"ui_ready\"\n      while executing\n  \"ui_ready $starting_gitk_msg\"\n      (\"after\" script)\n \nThe code that calls it (git-gui.sh:2211) looks like:\n\n  ui_status $::starting_gitk_msg\n  after 10000 {\n  \tui_ready $starting_gitk_msg\n  }\n\nI am not quite sure why this is done though. It was introduced in \ne210e67 (git-gui: Corrected keyboard bindings on Windows, improved state \nmanagement., 2006-11-06) [0], but the commit message doesn't really \nexplain why (probably because it is a small part of a larger change, \nthough it doesn't really fit in with the topic of the change). I can't \nfind a mailing list thread about the commit so I don't think we'll ever \nknow for sure.\n\nFrom looking at it, my guess is that it was added because gitk took a \nlong time to start up (maybe it still does, but for me its almost \ninstant). And so, this message was shown for 10 seconds, and then \ncleared because by then it probably would have started. But to avoid \nover-writing some other message, 'test' was used to make sure only the \nmessage intended to be cleared is cleared.\n\nI'm not sure if this heuristic/hack is really needed, and that we need \nto keep the \"Starting gitk...\" message around for 10 seconds. The way I \nsee it, it doesn't add too much value unless gitk takes a long time to \nstart up on other platforms or repos. In that case an indication of \n\"we're working on starting gitk\" would be nice. Otherwise, I don't mind \nseeing this go. And even then, I think it is gitk's responsibility to \ngive some sort of indication to the user that it is booting up, and not \nours.\n\nSo, I vote for just getting rid of this hack.\n\n>  \tglobal main_status\n>  \tif {[info exists main_status]} {\n> -\t\t$main_status show [mc \"Ready.\"] $test\n> +\t\t$main_status show [mc \"Ready.\"]\n>  \t}\n>  }\n>  \n> @@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n>  \tdo_explore\n>  }\n>  \n> +# Clear \"Initializing...\" status\n> +after 500 {$main_status show \"\"}\n> +\n>  # Local variables:\n>  # mode: tcl\n>  # indent-tabs-mode: t\n> diff --git a/lib/blame.tcl b/lib/blame.tcl\n> index a1aeb8b96e..888f98bab2 100644\n> --- a/lib/blame.tcl\n> +++ b/lib/blame.tcl\n> @@ -24,6 +24,7 @@ field w_cviewer  ; # pane showing commit message\n>  field finder     ; # find mini-dialog frame\n>  field gotoline   ; # line goto mini-dialog frame\n>  field status     ; # status mega-widget instance\n> +field status_operation ; # status operation\n\nNitpick: The comment doesn't give any information the field name doesn't \nalready give. Either remove it or replace it with something more \ndescriptive.\n\n>  field old_height ; # last known height of $w.file_pane\n>  \n>  \n> @@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {\n>  \tpack $w_cviewer -expand 1 -fill both\n>  \n>  \tset status [::status_bar::new $w.status]\n> +\tset status_operation {}\n>  \n>  \tmenu $w.ctxm -tearoff 0\n>  \t$w.ctxm add command \\\n> @@ -602,16 +604,21 @@ method _exec_blame {cur_w cur_d options cur_s} {\n>  \t} else {\n>  \t\tlappend options $commit\n>  \t}\n> +\n> +\t# We may recurse in from another call to _exec_blame and already have\n> +\t# a status operation.\n\nThanks for being thorough enough to spot this :)\n\n> +\tif {$status_operation == {}} {\n> +\t\tset status_operation [$status start \\\n> +\t\t\t$cur_s \\\n> +\t\t\t[mc \"lines annotated\"]]\n\nThe call to this method from '_read_blame' specifies a different $cur_s. \nSo shouldn't we be destroying $status_operation (after stopping it), and \ncreating a new one?\n\n> +\t}\n> +\n>  \tlappend options -- $path\n>  \tset fd [eval git_read --nice blame $options]\n>  \tfconfigure $fd -blocking 0 -translation lf -encoding utf-8\n>  \tfileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]\n>  \tset current_fd $fd\n>  \tset blame_lines 0\n> -\n> -\t$status start \\\n> -\t\t$cur_s \\\n> -\t\t[mc \"lines annotated\"]\n>  }\n>  \n>  method _read_blame {fd cur_w cur_d} {\n\nYou did not update 'lib/choose_repository.tcl'. It still uses the old \nversion of the status bar. Other than that, the rest of the patch looks \ngood. Thanks.\n\n[0]:\n  Curiously, if I do 'git log -L 2208,+5:git-gui.sh' to find the origins \n  of the line, it leads me to the commit 25476c6. And looking at the \n  commit, it does indeed appear to be the origin of the line since the \n  line is in the post-image, and not the pre-image. But I accidentally \n  noticed the line in a parent of that commit. Looking further, it turns \n  out the line originated in e210e67. Probably a bug in some really old \n  versions of git. Interesting nonetheless.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"387250","messageId":"20191127220322.hcmn3kcb2vj7jf7p@yadavpratyush.com","threadId":"52154","inReplyTo":"d0d6593b421609445e8f2c794e1d3ca46ebed068.1574627876.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 3/3] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-27T22:03:22Z","receivedAt":"2019-11-27T22:03:31Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nOn 24/11/19 08:37PM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the revert_helper proc to check for untracked files as well as\n> changes, and then handle changes to be reverted and untracked files with\n> independent blocks of code. Prompt the user independently for untracked\n> files, since the underlying action is fundamentally different (rm -f).\n> If after deleting untracked files, the directory containing them becomes\n> empty, then remove the directory as well. Migrate unlocking of the index\n> out of _close_updateindex to a responsibility of the caller, to permit\n> paths that don't directly unlock the index, and refactor the error\n> handling added in d4e890e5 so that callers can make flow control\n> decisions in the event of errors. Update Tcl/Tk dependency from 8.4 to\n> 8.6 in git-gui.sh.\n> \n> A new proc delete_files takes care of actually deleting the files in\n> batches, using the Tcler's Wiki recommended approach for keeping the UI\n> responsive.\n> \n> Since the checkout_index and delete_files calls are both asynchronous\n> and could potentially complete in any order, a \"chord\" is used to\n> coordinate unlocking the index and returning the UI to a usable state\n> only after both operations are complete. The `SimpleChord` class,\n> based on TclOO (Tcl/Tk 8.6), is added in this commit.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  git-gui.sh    |   4 +-\n>  lib/chord.tcl | 160 +++++++++++++++++++\n>  lib/index.tcl | 422 ++++++++++++++++++++++++++++++++++++++++----------\n>  3 files changed, 502 insertions(+), 84 deletions(-)\n>  create mode 100644 lib/chord.tcl\n\nFrom what I can tell, this re-roll of the patch only has minor changes \nall of which look good. Thanks.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"387266","messageId":"CAPSOpYvv+QZORJoGSNPisE=S_bAtS5tFtodnK9sHZVuTqVVxFg@mail.gmail.com","threadId":"52154","inReplyTo":"20191127215503.x2lt2b3nce7q4yj2@yadavpratyush.com","subject":"Re: [PATCH v5 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-11-28T07:34:21Z","receivedAt":"2019-11-28T07:34:41Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Wed, Nov 27, 2019 at 3:59 PM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> On 24/11/19 08:37PM, Jonathan Gilbert via GitGitGadget wrote:\n> > -proc ui_ready {{test {}}} {\n> > +proc ui_ready {} {\n>\n> This is not quite correct. There is one user of 'ui_ready' that uses\n> 'test'. It is in git-gui.sh:2211. It is used when starting gitk. This\n> change breaks that call. 10 seconds after opening gitk via the\n> \"Visualise master's history\" option, I get the following error:\n>\n>   wrong # args: should be \"ui_ready\"\n>       while executing\n>   \"ui_ready $starting_gitk_msg\"\n>       (\"after\" script)\n[..]\n> I'm not sure if this heuristic/hack is really needed, and that we need\n> to keep the \"Starting gitk...\" message around for 10 seconds.\n[..]\n> So, I vote for just getting rid of this hack.\n\nOh geeze, I can't believe I missed this. This looks like it ought to\nbe relatively straightforward to port to the new operations, though,\nwhich is a more isolated approach (keeping this change's fingers where\nthey belong), and then the operation provides segregation that means\nit can just be ended after X seconds without caring what anything else\nmight be doing with the status bar. We can independently figure out if\nwe want to restructure that part. Given that computers are faster now\nand that the status bar could end up doing something else in the\nmeantime (well let's be realistic, probably not, but who knows :-) ),\nI'd vote for reducing the time the message is shown from 10 seconds\nto, I dunno, 3 or 4 seconds.\n\nOne other thing I note is that both `do_gitk` and `do_git_gui` use\n`$starting_gitk_msg`, which means that when `do_git_gui` is invoked to\nlaunch a Git Gui child process for a submodule, it will be setting the\nstatus bar text to say that it is launching Gitk.\n\nSpeaking of things that are out of scope for this PR, I did notice\nthis in the code:\n\n> # -- Always start git gui through whatever we were loaded with.  This\n> #    lets us bypass using shell process on Windows systems.\n> #\n> set exe [list [_which git]]\n\nAs far as I can tell, there's virtually no connection between the\ncomment and what the code is actually doing. I haven't yet figured out\nwhat exactly it is or where it comes from, but on my Windows systems,\n`git-gui` is actually an EXE file `git-gui.exe`, and I _think_ what it\nis doing is running `wish.exe`, which I'm guessing has something to do\nwith hosting a Tcl interpreter with Win32 support for Tk GUI.\n\nI'm not sure whether the code is doing the right thing here or not,\nbut I'm pretty sure what it's _not_ doing is figuring out how the\ncurrent `git-gui` process was started/is being hosted. :-P\n\n> >  field finder     ; # find mini-dialog frame\n> >  field gotoline   ; # line goto mini-dialog frame\n> >  field status     ; # status mega-widget instance\n> > +field status_operation ; # status operation\n>\n> Nitpick: The comment doesn't give any information the field name doesn't\n> already give. Either remove it or replace it with something more\n> descriptive.\n\nHmm, okay. I didn't want something that felt wildly imbalanced with\nrespect to the other lines, but you're right that this particular line\nis literally just repeating the variable name. :-P\n\n> > +     if {$status_operation == {}} {\n> > +             set status_operation [$status start \\\n> > +                     $cur_s \\\n> > +                     [mc \"lines annotated\"]]\n>\n> The call to this method from '_read_blame' specifies a different $cur_s.\n> So shouldn't we be destroying $status_operation (after stopping it), and\n> creating a new one?\n\nWe can change the text by calling `$status_operation show`.\n\n> >  method _read_blame {fd cur_w cur_d} {\n>\n> You did not update 'lib/choose_repository.tcl'. It still uses the old\n> version of the status bar. Other than that, the rest of the patch looks\n> good. Thanks.\n\nUgh, I can't believe I overlooked this. I was aware of the file using\nthe status bar, because it's the one place that uses the `two_line`\nconstructor, but then I forgot to actually make it create and use the\n(single concurrent) operation that a `two_line`-er is allowed.\n\nThe code in there seems to overload the purpose of the `o_cons`\nvariable, so that sometimes it is pointing at a status bar and\nsometimes it is pointing at whatever `console::embed` returns. I will\nchange this.\n\nThis code also depends heavily on `update` to keep the UI active,\nwhich as I understand it is problematic because it could potentially\nresult in re-entrance since the user can interact with the UI in the\nmiddle of the operation. I will not make any attempt to change this,\nthough. :-)\n\n> [0]:\n>   Curiously, if I do 'git log -L 2208,+5:git-gui.sh' to find the origins\n>   of the line, it leads me to the commit 25476c6. And looking at the\n>   commit, it does indeed appear to be the origin of the line since the\n>   line is in the post-image, and not the pre-image. But I accidentally\n>   noticed the line in a parent of that commit. Looking further, it turns\n>   out the line originated in e210e67. Probably a bug in some really old\n>   versions of git. Interesting nonetheless.\n\nIn e210e67, I see this:\n\nset starting_gitk_msg {Please wait... Starting gitk...}\nproc do_gitk {} {\n        global tcl_platform ui_status_value starting_gitk_msg\n\n        set ui_status_value $starting_gitk_msg\n        after 5000 {\n                if {$ui_status_value == $starting_gitk_msg} {\n                        set ui_status_value {Ready.}\n                }\n        }\n        ...\n\nIn 043f7011, all string comparisons were changed from ==/!= to eq/ne.\nThe commit message explains that when you use == and !=, Tcl will\nattempt to convert either side to numeric if one of the two sides\nlooks like a numeric. Guess I should review my commit for this error\n:-P\n\n-                if {$ui_status_value == $starting_gitk_msg} {\n+                if {$ui_status_value eq $starting_gitk_msg} {\n\nIn 699d5601 \"Refactor our ui_status_value update technique\", this became:\n\nset starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n...\n        global ... starting_gitk_msg\n...\n        ui_status $starting_gitk_msg\n        after 10000 {\n                ui_ready $starting_gitk_msg\n        }\n\nFinally it became this in 02efd48f, apparently an unrelated\nrefactoring removed the global variable declaration:\n\nset starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n...\n        ui_status $::starting_gitk_msg\n        after 10000 {\n                ui_ready $starting_gitk_msg\n        }\n\nI gathered this information using Git Gui's blame function, which I\nguess is a good demonstration that my latest blame.tcl revision\ncorrects the problems in the earlier submission :-D\n\nNext revision coming soon.\n\nJonathan Gilbert\n"},{"id":"387267","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1574929833.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v6.git.1574929833.gitgitgadget@gmail.com","subject":"[PATCH v6 1/3] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-28T08:30:31Z","receivedAt":"2019-11-28T08:30:41Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"387268","messageId":"pull.436.v6.git.1574929833.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v5.git.1574627876.gitgitgadget@gmail.com","subject":"[PATCH v6 0/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-28T08:30:30Z","receivedAt":"2019-11-28T08:30:42Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nSince the checkout_index and delete_files calls are both asynchronous and\noverlap, they clash in wanting to update the status bar. To address this,\nthe status bar is reworked so that when an operation wants to display\nongoing updates/progress, it explicitly starts an \"operation\", which is\ntracked by its own object, and the status bar handles multiple concurrent\noperations by merging their progress and concatenating their text. This is\ncaptured in a separate commit, since it touches a variety of files.\n\nThe _close_updateindex proc contains error handling (added in d4e890e5) that\nhas the potential to interact badly with unlock_index running at the\ncompletion of an async operation. I have refactored the procedure into\nseparate procs _close_updateindex and rescan_on_error. Call sites that\nexercised the combined functionality also unlocked the index, so a combined\nproc close_and_unlock_index calls _close_updateindex and then either \nrescan_on_error or unlock_index as appropriate. Call sites have been updated\nappropriately.\n\nThe revert_helper proc, with its overlapping operations, is an example of a\ncall site that does not combine the close and unlock/rescan operations. The \ncheckout_index proc has been reworked to only call _close_updateindex, and\nto call a functor supplied by the caller to captures any errors that occur. \nrevert_helper uses this to supply a lambda function that stashes the error\nwithin the chord's body namespace, so that it can then separately call \nrescan_on_error when the chord is completed (or unlock_index, if no error\nwas captured), which might be substantially after checkout_index encounters\nits error. If it turns out that a rescan is called for, it is done once the\ndeletion is complete.\n\nThis is the seventh revision of this change, which differs from the sixth\nversion in the following ways (all related to the second commit updating the\nstatus bar):\n\n * The do_gitk and do_git_gui helper functions in git-gui.sh have been\n   updated to use status bar operations to show their \"Starting\" messages.\n   This eliminates the need for global variable starting_gitk_msg, and\n   corrects an issue where do_git_gui would say it was starting gitk rather\n   than git-gui by generalizing the localized message so the tool name can\n   be injected. The time to the message being removed is reduced to 3.5\n   seconds.\n   \n   \n * In blame.tcl, the comment on status_operation is made clearer.\n   \n   \n * In blame.tcl, when the status operation $cur_s string changes and there's\n   already an existing status bar operation, its text is updated to match.\n   \n   \n * The choose_repository.tcl file has been updated to work with the new\n   status bar model. There were some unnecessary complications that have\n   been sorted out: The o_cons field is no longer overloaded in meaning, and\n   the lifetimes of different status bar widgets is explicitly documented\n   (one gets created and then destroyed during _do_clone2, and another\n   separate one is greated during do_clone_checkout).\n   \n   \n * In choose_repository.tcl, there is a sequence of functions involved\n   performing the checkout on the clone: _do_clone_checkout => \n   _readtree_wait => _postcheckout_wait => _do_clone_submodules => \n   _do_validate_submodule_cloning. The functions have been re-ordered in the\n   source code to match the sequence in which they execute to improve\n   clarity.\n   \n   \n * The two_line constructor in status_bar.tcl now also initializes \n   completed_operation_count.\n   \n   \n * The start method on the status_bar class now no longer requires units to\n   be supplied. If they are omitted, a blank string is used. It is assumed\n   that the caller will not be calling update or update_meter in this\n   circumstance (though nothing should break if they do, it just won't show\n   any units).\n   \n   \n\ngit remote add logiclrd https://github.com/logiclrd/git.git\ngit fetch logiclrd git-gui-revert-untracked revision6\ngit diff d0d6593b42..3388407871\n\nJonathan Gilbert (3):\n  git-gui: consolidate naming conventions\n  git-gui: update status bar to track operations\n  git-gui: revert untracked files by deleting them\n\n git-gui.sh                |  31 ++-\n lib/blame.tcl             |  24 +-\n lib/checkout_op.tcl       |  15 +-\n lib/choose_repository.tcl | 120 ++++++---\n lib/chord.tcl             | 160 ++++++++++++\n lib/index.tcl             | 523 ++++++++++++++++++++++++++++----------\n lib/merge.tcl             |  14 +-\n lib/status_bar.tcl        | 229 +++++++++++++++--\n 8 files changed, 889 insertions(+), 227 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v6\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v5:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n 2:  aa05a78d28 ! 2:  ab3d8e54c3 git-gui: update status bar to track operations\n     @@ -12,6 +12,17 @@\n       diff --git a/git-gui.sh b/git-gui.sh\n       --- a/git-gui.sh\n       +++ b/git-gui.sh\n     +@@\n     + ##\n     + ## Tcl/Tk sanity check\n     + \n     +-if {[catch {package require Tcl 8.4} err]\n     +- || [catch {package require Tk  8.4} err]\n     ++if {[catch {package require Tcl 8.6} err]\n     ++ || [catch {package require Tk  8.6} err]\n     + } {\n     + \tcatch {wm withdraw .}\n     + \ttk_messageBox \\\n      @@\n       \t}\n       }\n     @@ -25,6 +36,47 @@\n       \t}\n       }\n       \n     +@@\n     + ##\n     + ## ui commands\n     + \n     +-set starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n     +-\n     + proc do_gitk {revs {is_submodule false}} {\n     + \tglobal current_diff_path file_states current_diff_side ui_index\n     + \tglobal _gitdir _gitworktree\n     +@@\n     + \t\tset env(GIT_WORK_TREE) $_gitworktree\n     + \t\tcd $pwd\n     + \n     +-\t\tui_status $::starting_gitk_msg\n     +-\t\tafter 10000 {\n     +-\t\t\tui_ready $starting_gitk_msg\n     +-\t\t}\n     ++\t\tset status_operation [$::main_status \\\n     ++\t\t\tstart \\\n     ++\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n     ++\n     ++\t\tafter 3500 [list $status_operation stop]\n     + \t}\n     + }\n     + \n     +@@\n     + \t\tset env(GIT_WORK_TREE) $_gitworktree\n     + \t\tcd $pwd\n     + \n     +-\t\tui_status $::starting_gitk_msg\n     +-\t\tafter 10000 {\n     +-\t\t\tui_ready $starting_gitk_msg\n     +-\t\t}\n     ++\t\tset status_operation [$::main_status \\\n     ++\t\t\tstart \\\n     ++\t\t\t[mc \"Starting %s... please wait...\" \"git-gui\"]]\n     ++\n     ++\t\tafter 3500 [list $status_operation stop]\n     + \t}\n     + }\n     + \n      @@\n       \tdo_explore\n       }\n     @@ -43,7 +95,7 @@\n       field finder     ; # find mini-dialog frame\n       field gotoline   ; # line goto mini-dialog frame\n       field status     ; # status mega-widget instance\n     -+field status_operation ; # status operation\n     ++field status_operation ; # operation displayed by status mega-widget\n       field old_height ; # last known height of $w.file_pane\n       \n       \n     @@ -66,6 +118,8 @@\n      +\t\tset status_operation [$status start \\\n      +\t\t\t$cur_s \\\n      +\t\t\t[mc \"lines annotated\"]]\n     ++\t} else {\n     ++\t\t$status_operation show $cur_s\n      +\t}\n      +\n       \tlappend options -- $path\n     @@ -162,6 +216,255 @@\n       }\n       \n      \n     + diff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl\n     + --- a/lib/choose_repository.tcl\n     + +++ b/lib/choose_repository.tcl\n     +@@\n     + field w_next      ; # Next button\n     + field w_quit      ; # Quit button\n     + field o_cons      ; # Console object (if active)\n     ++\n     ++# Status mega-widget instance during _do_clone2 (used by _copy_files and\n     ++# _link_files). Widget is destroyed before _do_clone2 calls\n     ++# _do_clone_checkout\n     ++field o_status\n     ++\n     ++# Operation displayed by status mega-widget during _do_clone_checkout =>\n     ++# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n     ++# _do_validate_submodule_cloning. The status mega-widget is a difference\n     ++# instance than that stored in $o_status in earlier operations.\n     ++field o_status_op\n     ++\n     + field w_types     ; # List of type buttons in clone\n     + field w_recentlist ; # Listbox containing recent repositories\n     + field w_localpath  ; # Entry widget bound to local_path\n     +@@\n     + \n     + \tswitch -exact -- $clone_type {\n     + \thardlink {\n     +-\t\tset o_cons [status_bar::two_line $w_body]\n     ++\t\tset o_status [status_bar::two_line $w_body]\n     + \t\tpack $w_body -fill x -padx 10 -pady 10\n     + \n     +-\t\t$o_cons start \\\n     ++\t\tset status_op [$o_status start \\\n     + \t\t\t[mc \"Counting objects\"] \\\n     +-\t\t\t[mc \"buckets\"]\n     ++\t\t\t[mc \"buckets\"]]\n     + \t\tupdate\n     + \n     + \t\tif {[file exists [file join $objdir info alternates]]} {\n     +@@\n     + \t\t\t} err]} {\n     + \t\t\t\tcatch {cd $pwd}\n     + \t\t\t\t_clone_failed $this [mc \"Unable to copy objects/info/alternates: %s\" $err]\n     ++\t\t\t\t$status_op stop\n     + \t\t\t\treturn\n     + \t\t\t}\n     + \t\t}\n     +@@\n     + \t\t\t-directory [file join $objdir] ??]\n     + \t\tset bcnt [expr {[llength $buckets] + 2}]\n     + \t\tset bcur 1\n     +-\t\t$o_cons update $bcur $bcnt\n     ++\t\t$status_op update $bcur $bcnt\n     + \t\tupdate\n     + \n     + \t\tfile mkdir [file join .git objects pack]\n     +@@\n     + \t\t\t-directory [file join $objdir pack] *] {\n     + \t\t\tlappend tolink [file join pack $i]\n     + \t\t}\n     +-\t\t$o_cons update [incr bcur] $bcnt\n     ++\t\t$status_op update [incr bcur] $bcnt\n     + \t\tupdate\n     + \n     + \t\tforeach i $buckets {\n     +@@\n     + \t\t\t\t-directory [file join $objdir $i] *] {\n     + \t\t\t\tlappend tolink [file join $i $j]\n     + \t\t\t}\n     +-\t\t\t$o_cons update [incr bcur] $bcnt\n     ++\t\t\t$status_op update [incr bcur] $bcnt\n     + \t\t\tupdate\n     + \t\t}\n     +-\t\t$o_cons stop\n     ++\t\t$status_op stop\n     + \n     + \t\tif {$tolink eq {}} {\n     + \t\t\tinfo_popup [strcat \\\n     +@@\n     + \t\tif {!$i} return\n     + \n     + \t\tdestroy $w_body\n     ++\n     ++\t\tset o_status {}\n     + \t}\n     + \tfull {\n     + \t\tset o_cons [console::embed \\\n     +@@\n     + }\n     + \n     + method _copy_files {objdir tocopy} {\n     +-\t$o_cons start \\\n     ++\tset status_op [$o_status start \\\n     + \t\t[mc \"Copying objects\"] \\\n     +-\t\t[mc \"KiB\"]\n     ++\t\t[mc \"KiB\"]]\n     + \tset tot 0\n     + \tset cmp 0\n     + \tforeach p $tocopy {\n     +@@\n     + \n     + \t\t\t\twhile {![eof $f_in]} {\n     + \t\t\t\t\tincr cmp [fcopy $f_in $f_cp -size 16384]\n     +-\t\t\t\t\t$o_cons update \\\n     ++\t\t\t\t\t$status_op update \\\n     + \t\t\t\t\t\t[expr {$cmp / 1024}] \\\n     + \t\t\t\t\t\t[expr {$tot / 1024}]\n     + \t\t\t\t\tupdate\n     +@@\n     + \t\t\t\tclose $f_cp\n     + \t\t\t} err]} {\n     + \t\t\t_clone_failed $this [mc \"Unable to copy object: %s\" $err]\n     ++\t\t\t$status_op stop\n     + \t\t\treturn 0\n     + \t\t}\n     + \t}\n     ++\t$status_op stop\n     + \treturn 1\n     + }\n     + \n     + method _link_files {objdir tolink} {\n     + \tset total [llength $tolink]\n     +-\t$o_cons start \\\n     ++\tset status_op [$o_status start \\\n     + \t\t[mc \"Linking objects\"] \\\n     +-\t\t[mc \"objects\"]\n     ++\t\t[mc \"objects\"]]\n     + \tfor {set i 0} {$i < $total} {} {\n     + \t\tset p [lindex $tolink $i]\n     + \t\tif {[catch {\n     +@@\n     + \t\t\t\t\t[file join $objdir $p]\n     + \t\t\t} err]} {\n     + \t\t\t_clone_failed $this [mc \"Unable to hardlink object: %s\" $err]\n     ++\t\t\t$status_op stop\n     + \t\t\treturn 0\n     + \t\t}\n     + \n     + \t\tincr i\n     + \t\tif {$i % 5 == 0} {\n     +-\t\t\t$o_cons update $i $total\n     ++\t\t\t$status_op update $i $total\n     + \t\t\tupdate\n     + \t\t}\n     + \t}\n     ++\t$status_op stop\n     + \treturn 1\n     + }\n     + \n     +@@\n     + \t\treturn\n     + \t}\n     + \n     +-\tset o_cons [status_bar::two_line $w_body]\n     ++\tset status [status_bar::two_line $w_body]\n     + \tpack $w_body -fill x -padx 10 -pady 10\n     +-\t$o_cons start \\\n     ++\n     ++\t# We start the status operation here.\n     ++\t#\n     ++\t# This function calls _readtree_wait as a callback.\n     ++\t#\n     ++\t# _readtree_wait in turn either calls _do_clone_submodules directly,\n     ++\t# or calls _postcheckout_wait as a callback which then calls\n     ++\t# _do_clone_submodules.\n     ++\t#\n     ++\t# _do_clone_submodules calls _do_validate_submodule_cloning.\n     ++\t#\n     ++\t# _do_validate_submodule_cloning stops the status operation.\n     ++\t#\n     ++\t# There are no other calls into this chain from other code.\n     ++\n     ++\tset o_status_op [$status start \\\n     + \t\t[mc \"Creating working directory\"] \\\n     +-\t\t[mc \"files\"]\n     ++\t\t[mc \"files\"]]\n     + \n     + \tset readtree_err {}\n     + \tset fd [git_read --stderr read-tree \\\n     +@@\n     + \tfileevent $fd readable [cb _readtree_wait $fd]\n     + }\n     + \n     +-method _do_validate_submodule_cloning {ok} {\n     +-\tif {$ok} {\n     +-\t\t$o_cons done $ok\n     +-\t\tset done 1\n     +-\t} else {\n     +-\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n     +-\t}\n     +-}\n     +-\n     +-method _do_clone_submodules {} {\n     +-\tif {$recursive eq {true}} {\n     +-\t\tdestroy $w_body\n     +-\t\tset o_cons [console::embed \\\n     +-\t\t\t$w_body \\\n     +-\t\t\t[mc \"Cloning submodules\"]]\n     +-\t\tpack $w_body -fill both -expand 1 -padx 10\n     +-\t\t$o_cons exec \\\n     +-\t\t\t[list git submodule update --init --recursive] \\\n     +-\t\t\t[cb _do_validate_submodule_cloning]\n     +-\t} else {\n     +-\t\tset done 1\n     +-\t}\n     +-}\n     +-\n     + method _readtree_wait {fd} {\n     + \tset buf [read $fd]\n     +-\t$o_cons update_meter $buf\n     ++\t$o_status_op update_meter $buf\n     + \tappend readtree_err $buf\n     + \n     + \tfconfigure $fd -blocking 1\n     +@@\n     + \tfconfigure $fd_ph -blocking 0\n     + }\n     + \n     ++method _do_clone_submodules {} {\n     ++\tif {$recursive eq {true}} {\n     ++\t\t$o_status_op stop\n     ++\t\tset o_status_op {}\n     ++\n     ++\t\tdestroy $w_body\n     ++\n     ++\t\tset o_cons [console::embed \\\n     ++\t\t\t$w_body \\\n     ++\t\t\t[mc \"Cloning submodules\"]]\n     ++\t\tpack $w_body -fill both -expand 1 -padx 10\n     ++\t\t$o_cons exec \\\n     ++\t\t\t[list git submodule update --init --recursive] \\\n     ++\t\t\t[cb _do_validate_submodule_cloning]\n     ++\t} else {\n     ++\t\tset done 1\n     ++\t}\n     ++}\n     ++\n     ++method _do_validate_submodule_cloning {ok} {\n     ++\tif {$ok} {\n     ++\t\t$o_cons done $ok\n     ++\t\tset done 1\n     ++\t} else {\n     ++\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n     ++\t}\n     ++}\n     ++\n     + ######################################################################\n     + ##\n     + ## Open Existing Repository\n     +\n       diff --git a/lib/index.tcl b/lib/index.tcl\n       --- a/lib/index.tcl\n       +++ b/lib/index.tcl\n     @@ -413,6 +716,7 @@\n      +\n      +\tset baseline_text \"\"\n      +\tset operations [list]\n     ++\tset completed_operation_count 0\n      +\n       \t${NS}::frame $w\n       \t${NS}::label $w_l \\\n     @@ -442,7 +746,7 @@\n      +\t$this refresh\n      +}\n      +\n     -+method start {msg uds} {\n     ++method start {msg {uds {}}} {\n      +\tset baseline_text \"\"\n      +\n      +\tif {!$allow_multiple && [llength $operations]} {\n 3:  d0d6593b42 ! 3:  3388407871 git-gui: revert untracked files by deleting them\n     @@ -26,21 +26,6 @@\n      \n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n     - diff --git a/git-gui.sh b/git-gui.sh\n     - --- a/git-gui.sh\n     - +++ b/git-gui.sh\n     -@@\n     - ##\n     - ## Tcl/Tk sanity check\n     - \n     --if {[catch {package require Tcl 8.4} err]\n     -- || [catch {package require Tk  8.4} err]\n     -+if {[catch {package require Tcl 8.6} err]\n     -+ || [catch {package require Tk  8.6} err]\n     - } {\n     - \tcatch {wm withdraw .}\n     - \ttk_messageBox \\\n     -\n       diff --git a/lib/chord.tcl b/lib/chord.tcl\n       new file mode 100644\n       --- /dev/null\n\n-- \ngitgitgadget\n"},{"id":"387269","messageId":"ab3d8e54c3d3d5174fe222ee77101ab3b8e9cab8.1574929833.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v6.git.1574929833.gitgitgadget@gmail.com","subject":"[PATCH v6 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-28T08:30:32Z","receivedAt":"2019-11-28T08:30:43Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the status bar to track updates as individual \"operations\" that\ncan overlap. Update all call sites to interact with the new status bar\nmechanism. Update initialization to explicitly clear status text,\nsince otherwise it may persist across future operations.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh                |  31 +++---\n lib/blame.tcl             |  24 ++--\n lib/checkout_op.tcl       |  15 +--\n lib/choose_repository.tcl | 120 +++++++++++++-------\n lib/index.tcl             |  31 ++++--\n lib/merge.tcl             |  14 ++-\n lib/status_bar.tcl        | 229 +++++++++++++++++++++++++++++++++-----\n 7 files changed, 354 insertions(+), 110 deletions(-)\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex 0d21f5688b..6dcf6551b6 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n ##\n ## Tcl/Tk sanity check\n \n-if {[catch {package require Tcl 8.4} err]\n- || [catch {package require Tk  8.4} err]\n+if {[catch {package require Tcl 8.6} err]\n+ || [catch {package require Tk  8.6} err]\n } {\n \tcatch {wm withdraw .}\n \ttk_messageBox \\\n@@ -1797,10 +1797,10 @@ proc ui_status {msg} {\n \t}\n }\n \n-proc ui_ready {{test {}}} {\n+proc ui_ready {} {\n \tglobal main_status\n \tif {[info exists main_status]} {\n-\t\t$main_status show [mc \"Ready.\"] $test\n+\t\t$main_status show [mc \"Ready.\"]\n \t}\n }\n \n@@ -2150,8 +2150,6 @@ proc incr_font_size {font {amt 1}} {\n ##\n ## ui commands\n \n-set starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n-\n proc do_gitk {revs {is_submodule false}} {\n \tglobal current_diff_path file_states current_diff_side ui_index\n \tglobal _gitdir _gitworktree\n@@ -2206,10 +2204,11 @@ proc do_gitk {revs {is_submodule false}} {\n \t\tset env(GIT_WORK_TREE) $_gitworktree\n \t\tcd $pwd\n \n-\t\tui_status $::starting_gitk_msg\n-\t\tafter 10000 {\n-\t\t\tui_ready $starting_gitk_msg\n-\t\t}\n+\t\tset status_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n+\n+\t\tafter 3500 [list $status_operation stop]\n \t}\n }\n \n@@ -2240,10 +2239,11 @@ proc do_git_gui {} {\n \t\tset env(GIT_WORK_TREE) $_gitworktree\n \t\tcd $pwd\n \n-\t\tui_status $::starting_gitk_msg\n-\t\tafter 10000 {\n-\t\t\tui_ready $starting_gitk_msg\n-\t\t}\n+\t\tset status_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Starting %s... please wait...\" \"git-gui\"]]\n+\n+\t\tafter 3500 [list $status_operation stop]\n \t}\n }\n \n@@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n \tdo_explore\n }\n \n+# Clear \"Initializing...\" status\n+after 500 {$main_status show \"\"}\n+\n # Local variables:\n # mode: tcl\n # indent-tabs-mode: t\ndiff --git a/lib/blame.tcl b/lib/blame.tcl\nindex a1aeb8b96e..bfcacd5584 100644\n--- a/lib/blame.tcl\n+++ b/lib/blame.tcl\n@@ -24,6 +24,7 @@ field w_cviewer  ; # pane showing commit message\n field finder     ; # find mini-dialog frame\n field gotoline   ; # line goto mini-dialog frame\n field status     ; # status mega-widget instance\n+field status_operation ; # operation displayed by status mega-widget\n field old_height ; # last known height of $w.file_pane\n \n \n@@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {\n \tpack $w_cviewer -expand 1 -fill both\n \n \tset status [::status_bar::new $w.status]\n+\tset status_operation {}\n \n \tmenu $w.ctxm -tearoff 0\n \t$w.ctxm add command \\\n@@ -602,16 +604,23 @@ method _exec_blame {cur_w cur_d options cur_s} {\n \t} else {\n \t\tlappend options $commit\n \t}\n+\n+\t# We may recurse in from another call to _exec_blame and already have\n+\t# a status operation.\n+\tif {$status_operation == {}} {\n+\t\tset status_operation [$status start \\\n+\t\t\t$cur_s \\\n+\t\t\t[mc \"lines annotated\"]]\n+\t} else {\n+\t\t$status_operation show $cur_s\n+\t}\n+\n \tlappend options -- $path\n \tset fd [eval git_read --nice blame $options]\n \tfconfigure $fd -blocking 0 -translation lf -encoding utf-8\n \tfileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]\n \tset current_fd $fd\n \tset blame_lines 0\n-\n-\t$status start \\\n-\t\t$cur_s \\\n-\t\t[mc \"lines annotated\"]\n }\n \n method _read_blame {fd cur_w cur_d} {\n@@ -806,10 +815,11 @@ method _read_blame {fd cur_w cur_d} {\n \t\t\t\t[mc \"Loading original location annotations...\"]\n \t\t} else {\n \t\t\tset current_fd {}\n-\t\t\t$status stop [mc \"Annotation complete.\"]\n+\t\t\t$status_operation stop [mc \"Annotation complete.\"]\n+\t\t\tset status_operation {}\n \t\t}\n \t} else {\n-\t\t$status update $blame_lines $total_lines\n+\t\t$status_operation update $blame_lines $total_lines\n \t}\n } ifdeleted { catch {close $fd} }\n \n@@ -1124,7 +1134,7 @@ method _blameparent {} {\n \t\t\tset diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]\n \t\t}\n \t\tif {[catch {set fd [eval git_read $diffcmd]} err]} {\n-\t\t\t$status stop [mc \"Unable to display parent\"]\n+\t\t\t$status_operation stop [mc \"Unable to display parent\"]\n \t\t\terror_popup [strcat [mc \"Error loading diff:\"] \"\\n\\n$err\"]\n \t\t\treturn\n \t\t}\ndiff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\nindex a5228297db..21ea768d80 100644\n--- a/lib/checkout_op.tcl\n+++ b/lib/checkout_op.tcl\n@@ -341,9 +341,9 @@ method _readtree {} {\n \tglobal HEAD\n \n \tset readtree_d {}\n-\t$::main_status start \\\n+\tset status_bar_operation [$::main_status start \\\n \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n-\t\t[mc \"files checked out\"]\n+\t\t[mc \"files checked out\"]]\n \n \tset fd [git_read --stderr read-tree \\\n \t\t-m \\\n@@ -354,26 +354,27 @@ method _readtree {} {\n \t\t$new_hash \\\n \t\t]\n \tfconfigure $fd -blocking 0 -translation binary\n-\tfileevent $fd readable [cb _readtree_wait $fd]\n+\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n }\n \n-method _readtree_wait {fd} {\n+method _readtree_wait {fd status_bar_operation} {\n \tglobal current_branch\n \n \tset buf [read $fd]\n-\t$::main_status update_meter $buf\n+\t$status_bar_operation update_meter $buf\n \tappend readtree_d $buf\n \n \tfconfigure $fd -blocking 1\n \tif {![eof $fd]} {\n \t\tfconfigure $fd -blocking 0\n+\t\t$status_bar_operation stop\n \t\treturn\n \t}\n \n \tif {[catch {close $fd}]} {\n \t\tset err $readtree_d\n \t\tregsub {^fatal: } $err {} err\n-\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n+\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n \n $err\n@@ -384,7 +385,7 @@ $err\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$status_bar_operation stop\n \t_after_readtree $this\n }\n \ndiff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl\nindex 80f5a59bbb..1ea0c9f7b8 100644\n--- a/lib/choose_repository.tcl\n+++ b/lib/choose_repository.tcl\n@@ -9,6 +9,18 @@ field w_body      ; # Widget holding the center content\n field w_next      ; # Next button\n field w_quit      ; # Quit button\n field o_cons      ; # Console object (if active)\n+\n+# Status mega-widget instance during _do_clone2 (used by _copy_files and\n+# _link_files). Widget is destroyed before _do_clone2 calls\n+# _do_clone_checkout\n+field o_status\n+\n+# Operation displayed by status mega-widget during _do_clone_checkout =>\n+# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n+# _do_validate_submodule_cloning. The status mega-widget is a difference\n+# instance than that stored in $o_status in earlier operations.\n+field o_status_op\n+\n field w_types     ; # List of type buttons in clone\n field w_recentlist ; # Listbox containing recent repositories\n field w_localpath  ; # Entry widget bound to local_path\n@@ -659,12 +671,12 @@ method _do_clone2 {} {\n \n \tswitch -exact -- $clone_type {\n \thardlink {\n-\t\tset o_cons [status_bar::two_line $w_body]\n+\t\tset o_status [status_bar::two_line $w_body]\n \t\tpack $w_body -fill x -padx 10 -pady 10\n \n-\t\t$o_cons start \\\n+\t\tset status_op [$o_status start \\\n \t\t\t[mc \"Counting objects\"] \\\n-\t\t\t[mc \"buckets\"]\n+\t\t\t[mc \"buckets\"]]\n \t\tupdate\n \n \t\tif {[file exists [file join $objdir info alternates]]} {\n@@ -689,6 +701,7 @@ method _do_clone2 {} {\n \t\t\t} err]} {\n \t\t\t\tcatch {cd $pwd}\n \t\t\t\t_clone_failed $this [mc \"Unable to copy objects/info/alternates: %s\" $err]\n+\t\t\t\t$status_op stop\n \t\t\t\treturn\n \t\t\t}\n \t\t}\n@@ -700,7 +713,7 @@ method _do_clone2 {} {\n \t\t\t-directory [file join $objdir] ??]\n \t\tset bcnt [expr {[llength $buckets] + 2}]\n \t\tset bcur 1\n-\t\t$o_cons update $bcur $bcnt\n+\t\t$status_op update $bcur $bcnt\n \t\tupdate\n \n \t\tfile mkdir [file join .git objects pack]\n@@ -708,7 +721,7 @@ method _do_clone2 {} {\n \t\t\t-directory [file join $objdir pack] *] {\n \t\t\tlappend tolink [file join pack $i]\n \t\t}\n-\t\t$o_cons update [incr bcur] $bcnt\n+\t\t$status_op update [incr bcur] $bcnt\n \t\tupdate\n \n \t\tforeach i $buckets {\n@@ -717,10 +730,10 @@ method _do_clone2 {} {\n \t\t\t\t-directory [file join $objdir $i] *] {\n \t\t\t\tlappend tolink [file join $i $j]\n \t\t\t}\n-\t\t\t$o_cons update [incr bcur] $bcnt\n+\t\t\t$status_op update [incr bcur] $bcnt\n \t\t\tupdate\n \t\t}\n-\t\t$o_cons stop\n+\t\t$status_op stop\n \n \t\tif {$tolink eq {}} {\n \t\t\tinfo_popup [strcat \\\n@@ -747,6 +760,8 @@ method _do_clone2 {} {\n \t\tif {!$i} return\n \n \t\tdestroy $w_body\n+\n+\t\tset o_status {}\n \t}\n \tfull {\n \t\tset o_cons [console::embed \\\n@@ -781,9 +796,9 @@ method _do_clone2 {} {\n }\n \n method _copy_files {objdir tocopy} {\n-\t$o_cons start \\\n+\tset status_op [$o_status start \\\n \t\t[mc \"Copying objects\"] \\\n-\t\t[mc \"KiB\"]\n+\t\t[mc \"KiB\"]]\n \tset tot 0\n \tset cmp 0\n \tforeach p $tocopy {\n@@ -798,7 +813,7 @@ method _copy_files {objdir tocopy} {\n \n \t\t\t\twhile {![eof $f_in]} {\n \t\t\t\t\tincr cmp [fcopy $f_in $f_cp -size 16384]\n-\t\t\t\t\t$o_cons update \\\n+\t\t\t\t\t$status_op update \\\n \t\t\t\t\t\t[expr {$cmp / 1024}] \\\n \t\t\t\t\t\t[expr {$tot / 1024}]\n \t\t\t\t\tupdate\n@@ -808,17 +823,19 @@ method _copy_files {objdir tocopy} {\n \t\t\t\tclose $f_cp\n \t\t\t} err]} {\n \t\t\t_clone_failed $this [mc \"Unable to copy object: %s\" $err]\n+\t\t\t$status_op stop\n \t\t\treturn 0\n \t\t}\n \t}\n+\t$status_op stop\n \treturn 1\n }\n \n method _link_files {objdir tolink} {\n \tset total [llength $tolink]\n-\t$o_cons start \\\n+\tset status_op [$o_status start \\\n \t\t[mc \"Linking objects\"] \\\n-\t\t[mc \"objects\"]\n+\t\t[mc \"objects\"]]\n \tfor {set i 0} {$i < $total} {} {\n \t\tset p [lindex $tolink $i]\n \t\tif {[catch {\n@@ -827,15 +844,17 @@ method _link_files {objdir tolink} {\n \t\t\t\t\t[file join $objdir $p]\n \t\t\t} err]} {\n \t\t\t_clone_failed $this [mc \"Unable to hardlink object: %s\" $err]\n+\t\t\t$status_op stop\n \t\t\treturn 0\n \t\t}\n \n \t\tincr i\n \t\tif {$i % 5 == 0} {\n-\t\t\t$o_cons update $i $total\n+\t\t\t$status_op update $i $total\n \t\t\tupdate\n \t\t}\n \t}\n+\t$status_op stop\n \treturn 1\n }\n \n@@ -958,11 +977,26 @@ method _do_clone_checkout {HEAD} {\n \t\treturn\n \t}\n \n-\tset o_cons [status_bar::two_line $w_body]\n+\tset status [status_bar::two_line $w_body]\n \tpack $w_body -fill x -padx 10 -pady 10\n-\t$o_cons start \\\n+\n+\t# We start the status operation here.\n+\t#\n+\t# This function calls _readtree_wait as a callback.\n+\t#\n+\t# _readtree_wait in turn either calls _do_clone_submodules directly,\n+\t# or calls _postcheckout_wait as a callback which then calls\n+\t# _do_clone_submodules.\n+\t#\n+\t# _do_clone_submodules calls _do_validate_submodule_cloning.\n+\t#\n+\t# _do_validate_submodule_cloning stops the status operation.\n+\t#\n+\t# There are no other calls into this chain from other code.\n+\n+\tset o_status_op [$status start \\\n \t\t[mc \"Creating working directory\"] \\\n-\t\t[mc \"files\"]\n+\t\t[mc \"files\"]]\n \n \tset readtree_err {}\n \tset fd [git_read --stderr read-tree \\\n@@ -976,33 +1010,9 @@ method _do_clone_checkout {HEAD} {\n \tfileevent $fd readable [cb _readtree_wait $fd]\n }\n \n-method _do_validate_submodule_cloning {ok} {\n-\tif {$ok} {\n-\t\t$o_cons done $ok\n-\t\tset done 1\n-\t} else {\n-\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n-\t}\n-}\n-\n-method _do_clone_submodules {} {\n-\tif {$recursive eq {true}} {\n-\t\tdestroy $w_body\n-\t\tset o_cons [console::embed \\\n-\t\t\t$w_body \\\n-\t\t\t[mc \"Cloning submodules\"]]\n-\t\tpack $w_body -fill both -expand 1 -padx 10\n-\t\t$o_cons exec \\\n-\t\t\t[list git submodule update --init --recursive] \\\n-\t\t\t[cb _do_validate_submodule_cloning]\n-\t} else {\n-\t\tset done 1\n-\t}\n-}\n-\n method _readtree_wait {fd} {\n \tset buf [read $fd]\n-\t$o_cons update_meter $buf\n+\t$o_status_op update_meter $buf\n \tappend readtree_err $buf\n \n \tfconfigure $fd -blocking 1\n@@ -1050,6 +1060,34 @@ method _postcheckout_wait {fd_ph} {\n \tfconfigure $fd_ph -blocking 0\n }\n \n+method _do_clone_submodules {} {\n+\tif {$recursive eq {true}} {\n+\t\t$o_status_op stop\n+\t\tset o_status_op {}\n+\n+\t\tdestroy $w_body\n+\n+\t\tset o_cons [console::embed \\\n+\t\t\t$w_body \\\n+\t\t\t[mc \"Cloning submodules\"]]\n+\t\tpack $w_body -fill both -expand 1 -padx 10\n+\t\t$o_cons exec \\\n+\t\t\t[list git submodule update --init --recursive] \\\n+\t\t\t[cb _do_validate_submodule_cloning]\n+\t} else {\n+\t\tset done 1\n+\t}\n+}\n+\n+method _do_validate_submodule_cloning {ok} {\n+\tif {$ok} {\n+\t\t$o_cons done $ok\n+\t\tset done 1\n+\t} else {\n+\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n+\t}\n+}\n+\n ######################################################################\n ##\n ## Open Existing Repository\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..62f4773ef4 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -45,13 +45,13 @@ proc _close_updateindex {fd after} {\n \t\twm deiconify $w\n \t\ttkwait window $w\n \n-\t\t$::main_status stop\n+\t\t$::main_status stop_all\n \t\tunlock_index\n \t\trescan $after 0\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$::main_status stop_all\n \tunlock_index\n \tuplevel #0 $after\n }\n@@ -67,7 +67,7 @@ proc update_indexinfo {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index -z --index-info]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -81,15 +81,18 @@ proc update_indexinfo {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd path_list total_cnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -119,7 +122,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc update_index {msg path_list after} {\n@@ -133,7 +136,7 @@ proc update_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index --add --remove -z --stdin]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -147,15 +150,18 @@ proc update_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd path_list total_cnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -190,7 +196,7 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc checkout_index {msg path_list after} {\n@@ -204,7 +210,7 @@ proc checkout_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write checkout-index \\\n \t\t--index \\\n \t\t--quiet \\\n@@ -224,15 +230,18 @@ proc checkout_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd path_list total_cnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -253,7 +262,7 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\ndiff --git a/lib/merge.tcl b/lib/merge.tcl\nindex 9f253db5b3..8df8ffae55 100644\n--- a/lib/merge.tcl\n+++ b/lib/merge.tcl\n@@ -241,23 +241,27 @@ Continue with resetting the current changes?\"]\n \tif {[ask_popup $op_question] eq {yes}} {\n \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n \t\tfconfigure $fd -blocking 0 -translation binary\n-\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n-\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n+\t\tset status_bar_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Aborting\"] \\\n+\t\t\t[mc \"files reset\"]\n+\t\tfileevent $fd readable [namespace code [list \\\n+\t\t\t_reset_wait $fd $status_bar_operation]]\n \t} else {\n \t\tunlock_index\n \t}\n }\n \n-proc _reset_wait {fd} {\n+proc _reset_wait {fd status_bar_operation} {\n \tglobal ui_comm\n \n-\t$::main_status update_meter [read $fd]\n+\t$status_bar_operation update_meter [read $fd]\n \n \tfconfigure $fd -blocking 1\n \tif {[eof $fd]} {\n \t\tset fail [catch {close $fd} err]\n-\t\t$::main_status stop\n \t\tunlock_index\n+\t\t$status_bar_operation stop\n \n \t\t$ui_comm delete 0.0 end\n \t\t$ui_comm edit modified false\ndiff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\nindex 02111a1742..32193d23ec 100644\n--- a/lib/status_bar.tcl\n+++ b/lib/status_bar.tcl\n@@ -1,16 +1,42 @@\n # git-gui status bar mega-widget\n # Copyright (C) 2007 Shawn Pearce\n \n+# The status_bar class manages the entire status bar. It is possible for\n+# multiple overlapping asynchronous operations to want to display status\n+# simultaneously. Each one receives a status_bar_operation when it calls the\n+# start method, and the status bar combines all active operations into the\n+# line of text it displays. Most of the time, there will be at most one\n+# ongoing operation.\n+#\n+# Note that the entire status bar can be either in single-line or two-line\n+# mode, depending on the constructor. Multiple active operations are only\n+# supported for single-line status bars.\n+\n class status_bar {\n \n+field allow_multiple ; # configured at construction\n+\n field w         ; # our own window path\n field w_l       ; # text widget we draw messages into\n field w_c       ; # canvas we draw a progress bar into\n field c_pack    ; # script to pack the canvas with\n-field status  {}; # single line of text we show\n-field prefix  {}; # text we format into status\n-field units   {}; # unit of progress\n-field meter   {}; # current core git progress meter (if active)\n+\n+field baseline_text   ; # text to show if there are no operations\n+field status_bar_text ; # combined text for all operations\n+\n+field operations ; # list of current ongoing operations\n+\n+# The status bar can display a progress bar, updated when consumers call the\n+# update method on their status_bar_operation. When there are multiple\n+# operations, the status bar shows the combined status of all operations.\n+#\n+# When an overlapping operation completes, the progress bar is going to\n+# abruptly have one fewer operation in the calculation, causing a discontinuity.\n+# Therefore, whenever an operation completes, if it is not the last operation,\n+# this counter is increased, and the progress bar is calculated as though there\n+# were still another operation at 100%. When the last operation completes, this\n+# is reset to 0.\n+field completed_operation_count\n \n constructor new {path} {\n \tglobal use_ttk NS\n@@ -18,12 +44,19 @@ constructor new {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Standard single-line status bar: Permit overlapping operations\n+\tset allow_multiple 1\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \tif {!$use_ttk} {\n \t\t$w configure -borderwidth 1 -relief sunken\n \t}\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -side left\n@@ -44,9 +77,16 @@ constructor two_line {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Two-line status bar: Only one ongoing operation permitted.\n+\tset allow_multiple 0\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -anchor w -fill x\n@@ -56,7 +96,7 @@ constructor two_line {path} {\n \treturn $this\n }\n \n-method start {msg uds} {\n+method ensure_canvas {} {\n \tif {[winfo exists $w_c]} {\n \t\t$w_c coords bar 0 0 0 20\n \t} else {\n@@ -68,31 +108,170 @@ method start {msg uds} {\n \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n \t\teval $c_pack\n \t}\n+}\n+\n+method show {msg} {\n+\t$this ensure_canvas\n+\tset baseline_text $msg\n+\t$this refresh\n+}\n+\n+method start {msg {uds {}}} {\n+\tset baseline_text \"\"\n+\n+\tif {!$allow_multiple && [llength $operations]} {\n+\t\treturn [lindex $operations 0]\n+\t}\n+\n+\t$this ensure_canvas\n+\n+\tset operation [status_bar_operation::new $this $msg $uds]\n+\n+\tlappend operations $operation\n+\n+\t$this refresh\n+\n+\treturn $operation\n+}\n+\n+method refresh {} {\n+\tset new_text \"\"\n+\n+\tset total [expr $completed_operation_count * 100]\n+\tset have $total\n+\n+\tforeach operation $operations {\n+\t\tif {$new_text != \"\"} {\n+\t\t\tappend new_text \" / \"\n+\t\t}\n+\n+\t\tappend new_text [$operation get_status]\n+\n+\t\tset total [expr $total + 100]\n+\t\tset have [expr $have + [$operation get_progress]]\n+\t}\n+\n+\tif {$new_text == \"\"} {\n+\t\tset new_text $baseline_text\n+\t}\n+\n+\tset status_bar_text $new_text\n+\n+\tif {[winfo exists $w_c]} {\n+\t\tset pixel_width 0\n+\t\tif {$have > 0} {\n+\t\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n+\t\t}\n+\n+\t\t$w_c coords bar 0 0 $pixel_width 20\n+\t}\n+}\n+\n+method stop {operation stop_msg} {\n+\tset idx [lsearch $operations $operation]\n+\n+\tif {$idx >= 0} {\n+\t\tset operations [lreplace $operations $idx $idx]\n+\t\tset completed_operation_count [expr \\\n+\t\t\t$completed_operation_count + 1]\n+\n+\t\tif {[llength $operations] == 0} {\n+\t\t\tset completed_operation_count 0\n+\n+\t\t\tdestroy $w_c\n+\t\t\tif {$stop_msg ne {}} {\n+\t\t\t\tset baseline_text $stop_msg\n+\t\t\t}\n+\t\t}\n+\n+\t\t$this refresh\n+\t}\n+}\n+\n+method stop_all {{stop_msg {}}} {\n+\t# This makes the operation's call to stop a no-op.\n+\tset operations_copy $operations\n+\tset operations [list]\n+\n+\tforeach operation $operations_copy {\n+\t\t$operation stop\n+\t}\n+\n+\tif {$stop_msg ne {}} {\n+\t\tset baseline_text $stop_msg\n+\t}\n+\n+\t$this refresh\n+}\n+\n+method _delete {current} {\n+\tif {$current eq $w} {\n+\t\tdelete_this\n+\t}\n+}\n+\n+}\n+\n+# The status_bar_operation class tracks a single consumer's ongoing status bar\n+# activity, with the context that there are a few situations where multiple\n+# overlapping asynchronous operations might want to display status information\n+# simultaneously. Instances of status_bar_operation are created by calling\n+# start on the status_bar, and when the caller is done with its stauts bar\n+# operation, it calls stop on the operation.\n+\n+class status_bar_operation {\n+\n+field status_bar; # reference back to the status_bar that owns this object\n+\n+field is_active;\n+\n+field status   {}; # single line of text we show\n+field progress {}; # current progress (0 to 100)\n+field prefix   {}; # text we format into status\n+field units    {}; # unit of progress\n+field meter    {}; # current core git progress meter (if active)\n+\n+constructor new {owner msg uds} {\n+\tset status_bar $owner\n \n \tset status $msg\n+\tset progress 0\n \tset prefix $msg\n \tset units  $uds\n \tset meter  {}\n+\n+\tset is_active 1\n+\n+\treturn $this\n }\n \n+method get_is_active {} { return $is_active }\n+method get_status {} { return $status }\n+method get_progress {} { return $progress }\n+\n method update {have total} {\n-\tset pdone 0\n-\tset cdone 0\n+\tif {!$is_active} { return }\n+\n+\tset progress 0\n+\n \tif {$total > 0} {\n-\t\tset pdone [expr {100 * $have / $total}]\n-\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n+\t\tset progress [expr {100 * $have / $total}]\n \t}\n \n \tset prec [string length [format %i $total]]\n+\n \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n \t\t$prefix \\\n \t\t$prec $have \\\n \t\t$prec $total \\\n-\t\t$units $pdone]\n-\t$w_c coords bar 0 0 $cdone 20\n+\t\t$units $progress]\n+\n+\t$status_bar refresh\n }\n \n method update_meter {buf} {\n+\tif {!$is_active} { return }\n+\n \tappend meter $buf\n \tset r [string last \"\\r\" $meter]\n \tif {$r == -1} {\n@@ -109,23 +288,23 @@ method update_meter {buf} {\n \t}\n }\n \n-method stop {{msg {}}} {\n-\tdestroy $w_c\n-\tif {$msg ne {}} {\n-\t\tset status $msg\n+method stop {{stop_msg {}}} {\n+\tif {$is_active} {\n+\t\tset is_active 0\n+\t\t$status_bar stop $this $stop_msg\n \t}\n }\n \n-method show {msg {test {}}} {\n-\tif {$test eq {} || $status eq $test} {\n-\t\tset status $msg\n-\t}\n+method show {msg} {\n+\tif {!$is_active} { return }\n+\n+\tset status $msg\n+\t$status_bar refresh\n }\n \n-method _delete {current} {\n-\tif {$current eq $w} {\n-\t\tdelete_this\n-\t}\n+method _delete {} {\n+\tstop\n+\tdelete_this\n }\n \n }\n-- \ngitgitgadget\n\n"},{"id":"387270","messageId":"3388407871f2e209354df03669d7efd78978bed3.1574929833.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v6.git.1574929833.gitgitgadget@gmail.com","subject":"[PATCH v6 3/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-11-28T08:30:33Z","receivedAt":"2019-11-28T08:30:45Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper proc to check for untracked files as well as\nchanges, and then handle changes to be reverted and untracked files with\nindependent blocks of code. Prompt the user independently for untracked\nfiles, since the underlying action is fundamentally different (rm -f).\nIf after deleting untracked files, the directory containing them becomes\nempty, then remove the directory as well. Migrate unlocking of the index\nout of _close_updateindex to a responsibility of the caller, to permit\npaths that don't directly unlock the index, and refactor the error\nhandling added in d4e890e5 so that callers can make flow control\ndecisions in the event of errors. Update Tcl/Tk dependency from 8.4 to\n8.6 in git-gui.sh.\n\nA new proc delete_files takes care of actually deleting the files in\nbatches, using the Tcler's Wiki recommended approach for keeping the UI\nresponsive.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand could potentially complete in any order, a \"chord\" is used to\ncoordinate unlocking the index and returning the UI to a usable state\nonly after both operations are complete. The `SimpleChord` class,\nbased on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/chord.tcl | 160 +++++++++++++++++++\n lib/index.tcl | 422 ++++++++++++++++++++++++++++++++++++++++----------\n 2 files changed, 500 insertions(+), 82 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..275a6cd4a1\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,160 @@\n+# Simple Chord for Tcl\n+#\n+# A \"chord\" is a method with more than one entrypoint and only one body, such\n+# that the body runs only once all the entrypoints have been called by\n+# different asynchronous tasks. In this implementation, the chord is defined\n+# dynamically for each invocation. A SimpleChord object is created, supplying\n+# body script to be run when the chord is completed, and then one or more notes\n+# are added to the chord. Each note can be called like a proc, and returns\n+# immediately if the chord isn't yet complete. When the last remaining note is\n+# called, the body runs before the note returns.\n+#\n+# The SimpleChord class has a constructor that takes the body script, and a\n+# method add_note that returns a note object. Since the body script does not\n+# run in the context of the procedure that defined it, a mechanism is provided\n+# for injecting variables into the chord for use by the body script. The\n+# activation of a note is idempotent; multiple calls have the same effect as\n+# a simple call.\n+#\n+# If you are invoking asynchronous operations with chord notes as completion\n+# callbacks, and there is a possibility that earlier operations could complete\n+# before later ones are started, it is a good practice to create a \"common\"\n+# note on the chord that prevents it from being complete until you're certain\n+# you've added all the notes you need.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+oo::class create SimpleChord {\n+\tvariable notes body is_completed\n+\n+\t# Constructor:\n+\t#   set chord [SimpleChord new {body}]\n+\t#     Creates a new chord object with the specified body script. The\n+\t#     body script is evaluated at most once, when a note is activated\n+\t#     and the chord has no other non-activated notes.\n+\tconstructor {body} {\n+\t\tset notes [list]\n+\t\tmy eval [list set body $body]\n+\t\tset is_completed 0\n+\t}\n+\n+\t# Method:\n+\t#   $chord eval {script}\n+\t#     Runs the specified script in the same context (namespace) in which\n+\t#     the chord body will be evaluated. This can be used to set variable\n+\t#     values for the chord body to use.\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\t# Method:\n+\t#   set note [$chord add_note]\n+\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n+\t#     error if the chord is already completed, otherwise the chord is\n+\t#     updated so that the new note must also be activated before the\n+\t#     body is evaluated.\n+\tmethod add_note {} {\n+\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\t# This method is for internal use only and is intentionally undocumented.\n+\tmethod notify_note_activation {} {\n+\t\tif {!$is_completed} {\n+\t\t\tforeach note $notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset is_completed 1\n+\n+\t\t\tnamespace eval [self] $body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+oo::class create ChordNote {\n+\tvariable chord is_activated\n+\n+\t# Constructor:\n+\t#   Instances of ChordNote are created internally by calling add_note on\n+\t#   SimpleChord objects.\n+\tconstructor {chord} {\n+\t\tmy eval set chord $chord\n+\t\tset is_activated 0\n+\t}\n+\n+\t# Method:\n+\t#   [$note is_activated]\n+\t#     Returns true if this note has already been activated.\n+\tmethod is_activated {} {\n+\t\treturn $is_activated\n+\t}\n+\n+\t# Method:\n+\t#   $note\n+\t#     Activates the note, if it has not already been activated, and\n+\t#     completes the chord if there are no other notes awaiting\n+\t#     activation. Subsequent calls will have no further effect.\n+\t#\n+\t# NB: In TclOO, if an object is invoked like a method without supplying\n+\t#     any method name, then this internal method `unknown` is what\n+\t#     actually runs (with no parameters). It is used in the ChordNote\n+\t#     class for the purpose of allowing the note object to be called as\n+\t#     a function (see example above). (The `unknown` method can also be\n+\t#     used to support dynamic dispatch, but must take parameters to\n+\t#     identify the \"unknown\" method to be invoked. In this form, this\n+\t#     proc serves only to make instances behave directly like methods.)\n+\tmethod unknown {} {\n+\t\tif {!$is_activated} {\n+\t\t\tset is_activated 1\n+\t\t\t$chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 62f4773ef4..1254145634 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,53 +7,60 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n-\tglobal use_ttk NS\n-\tfconfigure $fd -blocking 1\n-\tif {[catch {close $fd} err]} {\n-\t\tset w .indexfried\n-\t\tDialog $w\n-\t\twm withdraw $w\n-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n-\t\t\t-width [string length $s] -relief flat \\\n-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n-\t\t\t-background [get_bg_color $w]\n-\t\t$w.msg tag configure bold -font font_uibold -justify center\n-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n-\t\t$w.msg insert end $s bold \\n\\n$err {}\n-\t\t$w.msg configure -state disabled\n-\n-\t\t${NS}::button $w.continue \\\n-\t\t\t-text [mc \"Continue\"] \\\n-\t\t\t-command [list destroy $w]\n-\t\t${NS}::button $w.unlock \\\n-\t\t\t-text [mc \"Unlock Index\"] \\\n-\t\t\t-command \"destroy $w; _delete_indexlock\"\n-\t\tgrid $w.msg - $w.vs -sticky news\n-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n-\t\tgrid columnconfigure $w 0 -weight 1\n-\t\tgrid rowconfigure $w 0 -weight 1\n-\n-\t\twm protocol $w WM_DELETE_WINDOW update\n-\t\tbind $w.continue <Visibility> \"\n-\t\t\tgrab $w\n-\t\t\tfocus %W\n-\t\t\"\n-\t\twm deiconify $w\n-\t\ttkwait window $w\n-\n-\t\t$::main_status stop_all\n+proc close_and_unlock_index {fd after} {\n+\tif {![catch {_close_updateindex $fd} err]} {\n \t\tunlock_index\n-\t\trescan $after 0\n-\t\treturn\n+\t\tuplevel #0 $after\n+\t} else {\n+\t\trescan_on_error $err $after\n \t}\n+}\n+\n+proc _close_updateindex {fd} {\n+\tfconfigure $fd -blocking 1\n+\tclose $fd\n+}\n+\n+proc rescan_on_error {err {after {}}} {\n+\tglobal use_ttk NS\n+\n+\tset w .indexfried\n+\tDialog $w\n+\twm withdraw $w\n+\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n+\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n+\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n+\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n+\t\t-width [string length $s] -relief flat \\\n+\t\t-borderwidth 0 -highlightthickness 0 \\\n+\t\t-background [get_bg_color $w]\n+\t$w.msg tag configure bold -font font_uibold -justify center\n+\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n+\t$w.msg insert end $s bold \\n\\n$err {}\n+\t$w.msg configure -state disabled\n+\n+\t${NS}::button $w.continue \\\n+\t\t-text [mc \"Continue\"] \\\n+\t\t-command [list destroy $w]\n+\t${NS}::button $w.unlock \\\n+\t\t-text [mc \"Unlock Index\"] \\\n+\t\t-command \"destroy $w; _delete_indexlock\"\n+\tgrid $w.msg - $w.vs -sticky news\n+\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n+\tgrid columnconfigure $w 0 -weight 1\n+\tgrid rowconfigure $w 0 -weight 1\n+\n+\twm protocol $w WM_DELETE_WINDOW update\n+\tbind $w.continue <Visibility> \"\n+\t\tgrab $w\n+\t\tfocus %W\n+\t\"\n+\twm deiconify $w\n+\ttkwait window $w\n \n \t$::main_status stop_all\n \tunlock_index\n-\tuplevel #0 $after\n+\trescan [concat $after [list ui_ready]] 0\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -93,7 +100,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -162,7 +169,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -199,7 +206,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg path_list after} {\n+proc checkout_index {msg path_list after capture_error} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n@@ -232,17 +239,33 @@ proc checkout_index {msg path_list after} {\n \t\t$batch \\\n \t\t$status_bar_operation \\\n \t\t$after \\\n+\t\t$capture_error \\\n \t\t]\n }\n \n proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n-\tafter} {\n+\tafter capture_error} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\n+\t\t# We do not unlock the index directly here because this\n+\t\t# operation expects to potentially run in parallel with file\n+\t\t# deletions scheduled by revert_helper. We're done with the\n+\t\t# update index, so we close it, but actually unlocking the index\n+\t\t# and dealing with potential errors is deferred to the chord\n+\t\t# body that runs when all async operations are completed.\n+\t\t#\n+\t\t# (See after_chord in revert_helper.)\n+\n+\t\tif {[catch {_close_updateindex $fd} err]} {\n+\t\t\tuplevel #0 $capture_error [list $err]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\n \t\treturn\n \t}\n \n@@ -397,66 +420,301 @@ proc do_add_all {} {\n \tadd_helper [mc \"Adding all changed files\"] $paths\n }\n \n+# Copied from TclLib package \"lambda\".\n+proc lambda {arguments body args} {\n+\treturn [list ::apply [list $arguments $body] {*}$args]\n+}\n+\n proc revert_helper {txt paths} {\n \tglobal file_states current_diff_path\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\t#\n+\t# The asynchronous operations are each indicated below by a comment\n+\t# before the code block that starts the async operation.\n+\tset after_chord [SimpleChord new {\n+\t\tif {[string trim $err] != \"\"} {\n+\t\t\trescan_on_error $err\n+\t\t} else {\n+\t\t\tunlock_index\n+\t\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\t\tui_ready\n+\t\t}\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# This function captures an error for processing when after_chord is\n+\t# completed. (The chord is curried into the lambda function.)\n+\tset capture_error [lambda \\\n+\t\t{chord error} \\\n+\t\t{ $chord eval [list set err $error] } \\\n+\t\t$after_chord]\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n-\tset after {}\n+\tset untracked_list [list]\n+\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n+\n+\t# Asynchronous operation: revert changes by checking them out afresh\n+\t# from the index.\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note] \\\n+\t\t\t\t$capture_error\n+\t\t}\n \t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t# Asynchronous operation: Deletion of untracked files.\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n \n-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\tset status_bar_operation [$::main_status \\\n+\t\tstart \\\n+\t\t[mc \"Deleting\"] \\\n+\t\t[mc \"files\"]]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$status_bar_operation \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size \\\n+\tstatus_bar_operation after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$status_bar_operation update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$status_bar_operation \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$status_bar_operation stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0)\n+\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"387357","messageId":"20191130230543.p5xtapnx5a56arng@yadavpratyush.com","threadId":"52154","inReplyTo":"ab3d8e54c3d3d5174fe222ee77101ab3b8e9cab8.1574929833.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 2/3] git-gui: update status bar to track operations","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-11-30T23:05:43Z","receivedAt":"2019-11-30T23:05:52Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nThanks for the re-roll.\n\nOn 28/11/19 08:30AM, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the status bar to track updates as individual \"operations\" that\n> can overlap. Update all call sites to interact with the new status bar\n> mechanism. Update initialization to explicitly clear status text,\n> since otherwise it may persist across future operations.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n>  git-gui.sh                |  31 +++---\n>  lib/blame.tcl             |  24 ++--\n>  lib/checkout_op.tcl       |  15 +--\n>  lib/choose_repository.tcl | 120 +++++++++++++-------\n>  lib/index.tcl             |  31 ++++--\n>  lib/merge.tcl             |  14 ++-\n>  lib/status_bar.tcl        | 229 +++++++++++++++++++++++++++++++++-----\n>  7 files changed, 354 insertions(+), 110 deletions(-)\n> \n> diff --git a/git-gui.sh b/git-gui.sh\n> index 0d21f5688b..6dcf6551b6 100755\n> --- a/git-gui.sh\n> +++ b/git-gui.sh\n> @@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n>  ##\n>  ## Tcl/Tk sanity check\n>  \n> -if {[catch {package require Tcl 8.4} err]\n> - || [catch {package require Tk  8.4} err]\n> +if {[catch {package require Tcl 8.6} err]\n> + || [catch {package require Tk  8.6} err]\n\nNitpick: Since TclOO is introduced in patch 3 (and the commit message of \npatch 3 mentions it), this hunk should be in that patch instead.\n\n>  } {\n>  \tcatch {wm withdraw .}\n>  \ttk_messageBox \\\n> @@ -1797,10 +1797,10 @@ proc ui_status {msg} {\n>  \t}\n>  }\n>  \n> -proc ui_ready {{test {}}} {\n> +proc ui_ready {} {\n>  \tglobal main_status\n>  \tif {[info exists main_status]} {\n> -\t\t$main_status show [mc \"Ready.\"] $test\n> +\t\t$main_status show [mc \"Ready.\"]\n>  \t}\n>  }\n>  \n> @@ -2150,8 +2150,6 @@ proc incr_font_size {font {amt 1}} {\n>  ##\n>  ## ui commands\n>  \n> -set starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n> -\n>  proc do_gitk {revs {is_submodule false}} {\n>  \tglobal current_diff_path file_states current_diff_side ui_index\n>  \tglobal _gitdir _gitworktree\n> @@ -2206,10 +2204,11 @@ proc do_gitk {revs {is_submodule false}} {\n>  \t\tset env(GIT_WORK_TREE) $_gitworktree\n>  \t\tcd $pwd\n>  \n> -\t\tui_status $::starting_gitk_msg\n> -\t\tafter 10000 {\n> -\t\t\tui_ready $starting_gitk_msg\n> -\t\t}\n> +\t\tset status_operation [$::main_status \\\n> +\t\t\tstart \\\n> +\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n> +\n> +\t\tafter 3500 [list $status_operation stop]\n>  \t}\n>  }\n>  \n> @@ -2240,10 +2239,11 @@ proc do_git_gui {} {\n>  \t\tset env(GIT_WORK_TREE) $_gitworktree\n>  \t\tcd $pwd\n>  \n> -\t\tui_status $::starting_gitk_msg\n> -\t\tafter 10000 {\n> -\t\t\tui_ready $starting_gitk_msg\n> -\t\t}\n> +\t\tset status_operation [$::main_status \\\n> +\t\t\tstart \\\n> +\t\t\t[mc \"Starting %s... please wait...\" \"git-gui\"]]\n> +\n> +\t\tafter 3500 [list $status_operation stop]\n>  \t}\n>  }\n\nLooks good. Thanks for the cleanup.\n\n>  \n> @@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n>  \tdo_explore\n>  }\n>  \n> +# Clear \"Initializing...\" status\n> +after 500 {$main_status show \"\"}\n> +\n>  # Local variables:\n>  # mode: tcl\n>  # indent-tabs-mode: t\n> diff --git a/lib/blame.tcl b/lib/blame.tcl\n> index a1aeb8b96e..bfcacd5584 100644\n> --- a/lib/blame.tcl\n> +++ b/lib/blame.tcl\n> @@ -24,6 +24,7 @@ field w_cviewer  ; # pane showing commit message\n>  field finder     ; # find mini-dialog frame\n>  field gotoline   ; # line goto mini-dialog frame\n>  field status     ; # status mega-widget instance\n> +field status_operation ; # operation displayed by status mega-widget\n>  field old_height ; # last known height of $w.file_pane\n>  \n>  \n> @@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {\n>  \tpack $w_cviewer -expand 1 -fill both\n>  \n>  \tset status [::status_bar::new $w.status]\n> +\tset status_operation {}\n>  \n>  \tmenu $w.ctxm -tearoff 0\n>  \t$w.ctxm add command \\\n> @@ -602,16 +604,23 @@ method _exec_blame {cur_w cur_d options cur_s} {\n>  \t} else {\n>  \t\tlappend options $commit\n>  \t}\n> +\n> +\t# We may recurse in from another call to _exec_blame and already have\n> +\t# a status operation.\n> +\tif {$status_operation == {}} {\n> +\t\tset status_operation [$status start \\\n> +\t\t\t$cur_s \\\n> +\t\t\t[mc \"lines annotated\"]]\n> +\t} else {\n> +\t\t$status_operation show $cur_s\n> +\t}\n\nIIUC, in the previous version, a 'start' would reset the \nprogress/\"meter\". But this change only resets the label, not the actual \nprogress, which I think is what the caller wanted. So I think this \nshould be a full re-start instead.\n\n> +\n>  \tlappend options -- $path\n>  \tset fd [eval git_read --nice blame $options]\n>  \tfconfigure $fd -blocking 0 -translation lf -encoding utf-8\n>  \tfileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]\n>  \tset current_fd $fd\n>  \tset blame_lines 0\n> -\n> -\t$status start \\\n> -\t\t$cur_s \\\n> -\t\t[mc \"lines annotated\"]\n>  }\n>  \n>  method _read_blame {fd cur_w cur_d} {\n> diff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl\n> index 80f5a59bbb..1ea0c9f7b8 100644\n> --- a/lib/choose_repository.tcl\n> +++ b/lib/choose_repository.tcl\n> @@ -9,6 +9,18 @@ field w_body      ; # Widget holding the center content\n>  field w_next      ; # Next button\n>  field w_quit      ; # Quit button\n>  field o_cons      ; # Console object (if active)\n> +\n> +# Status mega-widget instance during _do_clone2 (used by _copy_files and\n> +# _link_files). Widget is destroyed before _do_clone2 calls\n> +# _do_clone_checkout\n> +field o_status\n> +\n> +# Operation displayed by status mega-widget during _do_clone_checkout =>\n> +# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n> +# _do_validate_submodule_cloning. The status mega-widget is a difference\n> +# instance than that stored in $o_status in earlier operations.\n\nThe last sentence doesn't make a lot of sense to me. What is \"earlier \noperations\"? If this refers to previous versions of this file, then I \ndon't think such a comment belongs here. It should be in the commit \nmessage instead.\n\n> +field o_status_op\n> +\n>  field w_types     ; # List of type buttons in clone\n>  field w_recentlist ; # Listbox containing recent repositories\n>  field w_localpath  ; # Entry widget bound to local_path\n> @@ -659,12 +671,12 @@ method _do_clone2 {} {\n>  \n>  \tswitch -exact -- $clone_type {\n>  \thardlink {\n> -\t\tset o_cons [status_bar::two_line $w_body]\n> +\t\tset o_status [status_bar::two_line $w_body]\n>  \t\tpack $w_body -fill x -padx 10 -pady 10\n>  \n> -\t\t$o_cons start \\\n> +\t\tset status_op [$o_status start \\\n>  \t\t\t[mc \"Counting objects\"] \\\n> -\t\t\t[mc \"buckets\"]\n> +\t\t\t[mc \"buckets\"]]\n>  \t\tupdate\n>  \n>  \t\tif {[file exists [file join $objdir info alternates]]} {\n> @@ -689,6 +701,7 @@ method _do_clone2 {} {\n>  \t\t\t} err]} {\n>  \t\t\t\tcatch {cd $pwd}\n>  \t\t\t\t_clone_failed $this [mc \"Unable to copy objects/info/alternates: %s\" $err]\n> +\t\t\t\t$status_op stop\n>  \t\t\t\treturn\n>  \t\t\t}\n>  \t\t}\n> @@ -700,7 +713,7 @@ method _do_clone2 {} {\n>  \t\t\t-directory [file join $objdir] ??]\n>  \t\tset bcnt [expr {[llength $buckets] + 2}]\n>  \t\tset bcur 1\n> -\t\t$o_cons update $bcur $bcnt\n> +\t\t$status_op update $bcur $bcnt\n>  \t\tupdate\n>  \n>  \t\tfile mkdir [file join .git objects pack]\n> @@ -708,7 +721,7 @@ method _do_clone2 {} {\n>  \t\t\t-directory [file join $objdir pack] *] {\n>  \t\t\tlappend tolink [file join pack $i]\n>  \t\t}\n> -\t\t$o_cons update [incr bcur] $bcnt\n> +\t\t$status_op update [incr bcur] $bcnt\n>  \t\tupdate\n>  \n>  \t\tforeach i $buckets {\n> @@ -717,10 +730,10 @@ method _do_clone2 {} {\n>  \t\t\t\t-directory [file join $objdir $i] *] {\n>  \t\t\t\tlappend tolink [file join $i $j]\n>  \t\t\t}\n> -\t\t\t$o_cons update [incr bcur] $bcnt\n> +\t\t\t$status_op update [incr bcur] $bcnt\n>  \t\t\tupdate\n>  \t\t}\n> -\t\t$o_cons stop\n> +\t\t$status_op stop\n>  \n>  \t\tif {$tolink eq {}} {\n>  \t\t\tinfo_popup [strcat \\\n> @@ -747,6 +760,8 @@ method _do_clone2 {} {\n>  \t\tif {!$i} return\n>  \n>  \t\tdestroy $w_body\n> +\n> +\t\tset o_status {}\n\nShould we be calling a destructor for this here? There is the '_delete' \nmethod in status_bar.tcl, but I don't see any usages of it so I'm not \nsure what exactly it is supposed to do.\n\nThat said, the previous version of this file doesn't call any sort of \ndestructor either, so maybe we should just leave it like it is for now. \nI dunno.\n\n>  \t}\n>  \tfull {\n>  \t\tset o_cons [console::embed \\\n> @@ -976,33 +1010,9 @@ method _do_clone_checkout {HEAD} {\n>  \tfileevent $fd readable [cb _readtree_wait $fd]\n>  }\n>  \n> -method _do_validate_submodule_cloning {ok} {\n> -\tif {$ok} {\n> -\t\t$o_cons done $ok\n> -\t\tset done 1\n> -\t} else {\n> -\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n> -\t}\n> -}\n> -\n> -method _do_clone_submodules {} {\n> -\tif {$recursive eq {true}} {\n> -\t\tdestroy $w_body\n> -\t\tset o_cons [console::embed \\\n> -\t\t\t$w_body \\\n> -\t\t\t[mc \"Cloning submodules\"]]\n> -\t\tpack $w_body -fill both -expand 1 -padx 10\n> -\t\t$o_cons exec \\\n> -\t\t\t[list git submodule update --init --recursive] \\\n> -\t\t\t[cb _do_validate_submodule_cloning]\n> -\t} else {\n> -\t\tset done 1\n> -\t}\n> -}\n> -\n\nIs there a reason for moving these two methods around? Not that its a \nbad thing, I'm just curious.\n\n>  method _readtree_wait {fd} {\n>  \tset buf [read $fd]\n> -\t$o_cons update_meter $buf\n> +\t$o_status_op update_meter $buf\n>  \tappend readtree_err $buf\n>  \n>  \tfconfigure $fd -blocking 1\n\nEverything other than a couple of minor comments above looks good. \nThanks for the quality contribution. Looking forward to finally merging \nthe next and final version of the series :)\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"387362","messageId":"CAPSOpYs7GeKhcWLHtmwuXQWimp-Bgk8F5WYoO+XQM8C8dDj7pA@mail.gmail.com","threadId":"52154","inReplyTo":"20191130230543.p5xtapnx5a56arng@yadavpratyush.com","subject":"Re: [PATCH v6 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-12-01T02:12:55Z","receivedAt":"2019-12-01T02:13:19Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sat, Nov 30, 2019 at 5:05 PM Pratyush Yadav me-at-yadavpratyush.com\n|GitHub Public/Example Allow| <172q77k4bxwj0zt@sneakemail.com> wrote:\n> Hi Jonathan,\n>\n> Thanks for the re-roll.\n\nYou are most welcome :-)\n\n> On 28/11/19 08:30AM, Jonathan Gilbert via GitGitGadget wrote:\n> > +# Operation displayed by status mega-widget during _do_clone_checkout =>\n> > +# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n> > +# _do_validate_submodule_cloning. The status mega-widget is a difference\n> > +# instance than that stored in $o_status in earlier operations.\n>\n> The last sentence doesn't make a lot of sense to me. What is \"earlier\n> operations\"? If this refers to previous versions of this file, then I\n> don't think such a comment belongs here. It should be in the commit\n> message instead.\n\nA clone starts out by calling `_do_clone2`, which, for `$clone_type`\nof `hardlink`, creates a status \"mega-widget\" and uses it to track\nlinking and/or copying the underlying files. Then, this part of the UI\nis destroyed. Later, the code calls into _do_clone_checkout, which\nsets up its own, different view. This view _also_ uses a status\n\"mega-widget\", but it's not the same one as before. This wasn't\nobvious to me in my first read-through, and I erroneously wrote code\nthat assumed the widget objects would carry forward. As such, I felt\nit might be useful to other readers to have this detail called out\nup-front. In the context of `_do_clone_checkout`, the \"earlier\noperations\" is what happens in `_do_clone2`.\n\n> >               destroy $w_body\n> > +\n> > +             set o_status {}\n>\n> Should we be calling a destructor for this here? There is the '_delete'\n> method in status_bar.tcl, but I don't see any usages of it so I'm not\n> sure what exactly it is supposed to do.\n>\n> That said, the previous version of this file doesn't call any sort of\n> destructor either, so maybe we should just leave it like it is for now.\n> I dunno.\n\nAs far as I can tell, `destroy $w_body` automatically deletes the\nentire subtree of UI components. I mentioned that I had written broken\ncode at first because I didn't realize the status widget got replaced\nbetween `_do_clone2` and `_do_clone_checkout` -- that code encountered\nan error that indicated that the status widget object no longer\nexisted at all. Thus, I have proceeded on the assumption that `destroy\n$w_body` handles that particular detail, and all that's left is to\nclear `o_status` of its dangling reference to the object that no\nlonger exists.\n\n> > -method _do_validate_submodule_cloning {ok} {\n> > [..]\n> > -method _do_clone_submodules {} {\n>\n> Is there a reason for moving these two methods around? Not that its a\n> bad thing, I'm just curious.\n\nI touched on this in the cover letter. I'll just copy/paste that text\nsince it says it just as well as I could re-synthesize here :-)\n\n* In `choose_repository.tcl`, there is a sequence of functions\ninvolved performing the checkout on the clone: `_do_clone_checkout` =>\n`_readtree_wait` => `_postcheckout_wait` => `_do_clone_submodules` =>\n`_do_validate_submodule_cloning`. The functions have been re-ordered\nin the source code to match the sequence in which they execute to\nimprove clarity.\n\nRe-roll (final?) incoming.\n\nThanks,\n\nJonathan Gilbert\n"},{"id":"387363","messageId":"pull.436.v7.git.1575167313.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v6.git.1574929833.gitgitgadget@gmail.com","subject":"[PATCH v7 0/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-01T02:28:30Z","receivedAt":"2019-12-01T02:28:39Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"My development environment sometimes makes automatic changes that I don't\nwant to keep. In some cases, this involves new files being added that I\ndon't want to commit or keep (but I also don't want to outright .gitignore \nforever). I have typically had to explicitly delete those files externally\nto Git Gui, which is a context switch to a manual operation, and I want to\nbe able to just select those newly-created untracked files in the UI and\n\"revert\" them into oblivion.\n\nThis change updates the revert_helper proc to check for untracked files as\nwell as changes, and then changes to be reverted and untracked files are\nhandled by independent blocks of code. The user is prompted independently\nfor untracked files, since the underlying action is fundamentally different\n(rm -f). If after deleting untracked files, the directory containing them\nbecomes empty, then the directory is removed as well. A new proc \ndelete_files takes care of actually deleting the files, using the Tcler's\nWiki recommended approach for keeping the UI responsive.\n\nSince the checkout_index and delete_files calls are both asynchronous and\ncould potentially complete in any order, a \"chord\" is used to coordinate\nunlocking the index and returning the UI to a usable state only after both\noperations are complete.\n\nSince the checkout_index and delete_files calls are both asynchronous and\noverlap, they clash in wanting to update the status bar. To address this,\nthe status bar is reworked so that when an operation wants to display\nongoing updates/progress, it explicitly starts an \"operation\", which is\ntracked by its own object, and the status bar handles multiple concurrent\noperations by merging their progress and concatenating their text. This is\ncaptured in a separate commit, since it touches a variety of files.\n\nThe _close_updateindex proc contains error handling (added in d4e890e5) that\nhas the potential to interact badly with unlock_index running at the\ncompletion of an async operation. I have refactored the procedure into\nseparate procs _close_updateindex and rescan_on_error. Call sites that\nexercised the combined functionality also unlocked the index, so a combined\nproc close_and_unlock_index calls _close_updateindex and then either \nrescan_on_error or unlock_index as appropriate. Call sites have been updated\nappropriately.\n\nThe revert_helper proc, with its overlapping operations, is an example of a\ncall site that does not combine the close and unlock/rescan operations. The \ncheckout_index proc has been reworked to only call _close_updateindex, and\nto call a functor supplied by the caller to captures any errors that occur. \nrevert_helper uses this to supply a lambda function that stashes the error\nwithin the chord's body namespace, so that it can then separately call \nrescan_on_error when the chord is completed (or unlock_index, if no error\nwas captured), which might be substantially after checkout_index encounters\nits error. If it turns out that a rescan is called for, it is done once the\ndeletion is complete.\n\nThis is the eighth revision of this change, which differs from the seventh\nversion in the following ways (most of which are in the second of the three\ncommits, to do with the status bar rework):\n\n * The bump of the Tcl/Tk dependency from 8.4 to 8.6 now takes place in the\n   third commit, where it is needed and whose commit message actually calls\n   it out.\n   \n   \n * The show method in status_bar_operation has been renamed to restart, and\n   the meter is cleared. Also, the supplied message is set as the prefix for\n   future update calls.\n   \n   \n * The call site for $status_operation show in blame.tcl has been\n   corresponding changed to $status_operation restart.\n   \n   \n * A typo has been corrected in a comment. :-)\n   \n   \n\ngit remote add logiclrd https://github.com/logiclrd/git.git\ngit fetch logiclrd git-gui-revert-untracked revision7\n# Compare the second commit from the past submission with the one\n# from this submission:\n# - revision7~ == ab3d8e54c3d\n# - git-gui-revert-untracked~ == 8fe9dfc30771\ngit diff ab3d8e54c3d..8fe9dfc30771\n\nJonathan Gilbert (3):\n  git-gui: consolidate naming conventions\n  git-gui: update status bar to track operations\n  git-gui: revert untracked files by deleting them\n\n git-gui.sh                |  31 ++-\n lib/blame.tcl             |  24 +-\n lib/checkout_op.tcl       |  15 +-\n lib/choose_repository.tcl | 120 ++++++---\n lib/chord.tcl             | 160 ++++++++++++\n lib/index.tcl             | 523 ++++++++++++++++++++++++++++----------\n lib/merge.tcl             |  14 +-\n lib/status_bar.tcl        | 231 +++++++++++++++--\n 8 files changed, 891 insertions(+), 227 deletions(-)\n create mode 100644 lib/chord.tcl\n\n\nbase-commit: b524f6b399c77b40c8bf2b6217585fde4731472a\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-436%2Flogiclrd%2Fgit-gui-revert-untracked-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-436/logiclrd/git-gui-revert-untracked-v7\nPull-Request: https://github.com/gitgitgadget/git/pull/436\n\nRange-diff vs v6:\n\n 1:  da1704c56e = 1:  da1704c56e git-gui: consolidate naming conventions\n 2:  ab3d8e54c3 ! 2:  8fe9dfc307 git-gui: update status bar to track operations\n     @@ -12,17 +12,6 @@\n       diff --git a/git-gui.sh b/git-gui.sh\n       --- a/git-gui.sh\n       +++ b/git-gui.sh\n     -@@\n     - ##\n     - ## Tcl/Tk sanity check\n     - \n     --if {[catch {package require Tcl 8.4} err]\n     -- || [catch {package require Tk  8.4} err]\n     -+if {[catch {package require Tcl 8.6} err]\n     -+ || [catch {package require Tk  8.6} err]\n     - } {\n     - \tcatch {wm withdraw .}\n     - \ttk_messageBox \\\n      @@\n       \t}\n       }\n     @@ -119,7 +108,7 @@\n      +\t\t\t$cur_s \\\n      +\t\t\t[mc \"lines annotated\"]]\n      +\t} else {\n     -+\t\t$status_operation show $cur_s\n     ++\t\t$status_operation restart $cur_s\n      +\t}\n      +\n       \tlappend options -- $path\n     @@ -231,7 +220,7 @@\n      +\n      +# Operation displayed by status mega-widget during _do_clone_checkout =>\n      +# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n     -+# _do_validate_submodule_cloning. The status mega-widget is a difference\n     ++# _do_validate_submodule_cloning. The status mega-widget is a different\n      +# instance than that stored in $o_status in earlier operations.\n      +field o_status_op\n      +\n     @@ -930,10 +919,12 @@\n      -\tif {$test eq {} || $status eq $test} {\n      -\t\tset status $msg\n      -\t}\n     -+method show {msg} {\n     ++method restart {msg} {\n      +\tif {!$is_active} { return }\n      +\n      +\tset status $msg\n     ++\tset prefix $msg\n     ++\tset meter {}\n      +\t$status_bar refresh\n       }\n       \n 3:  3388407871 ! 3:  5f8120f359 git-gui: revert untracked files by deleting them\n     @@ -26,6 +26,21 @@\n      \n          Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n      \n     + diff --git a/git-gui.sh b/git-gui.sh\n     + --- a/git-gui.sh\n     + +++ b/git-gui.sh\n     +@@\n     + ##\n     + ## Tcl/Tk sanity check\n     + \n     +-if {[catch {package require Tcl 8.4} err]\n     +- || [catch {package require Tk  8.4} err]\n     ++if {[catch {package require Tcl 8.6} err]\n     ++ || [catch {package require Tk  8.6} err]\n     + } {\n     + \tcatch {wm withdraw .}\n     + \ttk_messageBox \\\n     +\n       diff --git a/lib/chord.tcl b/lib/chord.tcl\n       new file mode 100644\n       --- /dev/null\n\n-- \ngitgitgadget\n"},{"id":"387364","messageId":"da1704c56e0bd3f1ac53af96b702e3777bb3eb37.1575167313.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v7.git.1575167313.gitgitgadget@gmail.com","subject":"[PATCH v7 1/3] git-gui: consolidate naming conventions","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-01T02:28:31Z","receivedAt":"2019-12-01T02:28:39Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nA few variables in this file use camelCase, while the overall standard\nis snake_case. A consistent naming scheme will improve readability of\nfuture changes. To avoid mixing naming changes with semantic changes,\nthis commit contains only naming changes.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n lib/index.tcl | 92 +++++++++++++++++++++++++--------------------------\n 1 file changed, 46 insertions(+), 46 deletions(-)\n\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex e07b7a3762..28d4d2a54e 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -56,15 +56,15 @@ proc _close_updateindex {fd after} {\n \tuplevel #0 $after\n }\n \n-proc update_indexinfo {msg pathList after} {\n+proc update_indexinfo {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -78,26 +78,26 @@ proc update_indexinfo {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_indexinfo \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd pathList totalCnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tset s $file_states($path)\n@@ -119,18 +119,18 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc update_index {msg pathList after} {\n+proc update_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -144,26 +144,26 @@ proc update_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_update_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd pathList totalCnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -190,18 +190,18 @@ proc write_update_index {fd pathList totalCnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg pathList after} {\n+proc checkout_index {msg path_list after} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n \n \tset update_index_cp 0\n-\tset pathList [lsort $pathList]\n-\tset totalCnt [llength $pathList]\n-\tset batch [expr {int($totalCnt * .01) + 1}]\n+\tset path_list [lsort $path_list]\n+\tset total_cnt [llength $path_list]\n+\tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n \t$::main_status start $msg [mc \"files\"]\n@@ -221,26 +221,26 @@ proc checkout_index {msg pathList after} {\n \tfileevent $fd writable [list \\\n \t\twrite_checkout_index \\\n \t\t$fd \\\n-\t\t$pathList \\\n-\t\t$totalCnt \\\n+\t\t$path_list \\\n+\t\t$total_cnt \\\n \t\t$batch \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd pathList totalCnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch after} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n-\tif {$update_index_cp >= $totalCnt} {\n+\tif {$update_index_cp >= $total_cnt} {\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n \n \tfor {set i $batch} \\\n-\t\t{$update_index_cp < $totalCnt && $i > 0} \\\n+\t\t{$update_index_cp < $total_cnt && $i > 0} \\\n \t\t{incr i -1} {\n-\t\tset path [lindex $pathList $update_index_cp]\n+\t\tset path [lindex $path_list $update_index_cp]\n \t\tincr update_index_cp\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n@@ -253,7 +253,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $totalCnt\n+\t$::main_status update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\n@@ -261,7 +261,7 @@ proc unstage_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -269,19 +269,19 @@ proc unstage_helper {txt paths} {\n \t\tM? -\n \t\tT? -\n \t\tD? {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_indexinfo \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t}\n }\n@@ -305,7 +305,7 @@ proc add_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -321,19 +321,19 @@ proc add_helper {txt paths} {\n \t\t?M -\n \t\t?D -\n \t\t?T {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n-\tif {$pathList eq {}} {\n+\tif {$path_list eq {}} {\n \t\tunlock_index\n \t} else {\n \t\tupdate_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after {ui_status [mc \"Ready to commit.\"]}]\n \t}\n }\n@@ -393,7 +393,7 @@ proc revert_helper {txt paths} {\n \n \tif {![lock_index begin-update]} return\n \n-\tset pathList [list]\n+\tset path_list [list]\n \tset after {}\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n@@ -401,7 +401,7 @@ proc revert_helper {txt paths} {\n \t\t?M -\n \t\t?T -\n \t\t?D {\n-\t\t\tlappend pathList $path\n+\t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n \t\t\t\tset after {reshow_diff;}\n \t\t\t}\n@@ -420,12 +420,12 @@ proc revert_helper {txt paths} {\n \t# as they have quite complex plural-form rules. Unfortunately,\n \t# msgcat doesn't seem to support that kind of string translation.\n \t#\n-\tset n [llength $pathList]\n+\tset n [llength $path_list]\n \tif {$n == 0} {\n \t\tunlock_index\n \t\treturn\n \t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $pathList]]]\n+\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n \t} else {\n \t\tset query [mc \"Revert changes in these %i files?\" $n]\n \t}\n@@ -444,7 +444,7 @@ proc revert_helper {txt paths} {\n \tif {$reply == 1} {\n \t\tcheckout_index \\\n \t\t\t$txt \\\n-\t\t\t$pathList \\\n+\t\t\t$path_list \\\n \t\t\t[concat $after [list ui_ready]]\n \t} else {\n \t\tunlock_index\n-- \ngitgitgadget\n\n"},{"id":"387365","messageId":"5f8120f35974c5c404953c33a47016f8eaae454a.1575167313.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v7.git.1575167313.gitgitgadget@gmail.com","subject":"[PATCH v7 3/3] git-gui: revert untracked files by deleting them","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-01T02:28:33Z","receivedAt":"2019-12-01T02:28:42Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the revert_helper proc to check for untracked files as well as\nchanges, and then handle changes to be reverted and untracked files with\nindependent blocks of code. Prompt the user independently for untracked\nfiles, since the underlying action is fundamentally different (rm -f).\nIf after deleting untracked files, the directory containing them becomes\nempty, then remove the directory as well. Migrate unlocking of the index\nout of _close_updateindex to a responsibility of the caller, to permit\npaths that don't directly unlock the index, and refactor the error\nhandling added in d4e890e5 so that callers can make flow control\ndecisions in the event of errors. Update Tcl/Tk dependency from 8.4 to\n8.6 in git-gui.sh.\n\nA new proc delete_files takes care of actually deleting the files in\nbatches, using the Tcler's Wiki recommended approach for keeping the UI\nresponsive.\n\nSince the checkout_index and delete_files calls are both asynchronous\nand could potentially complete in any order, a \"chord\" is used to\ncoordinate unlocking the index and returning the UI to a usable state\nonly after both operations are complete. The `SimpleChord` class,\nbased on TclOO (Tcl/Tk 8.6), is added in this commit.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh    |   4 +-\n lib/chord.tcl | 160 +++++++++++++++++++\n lib/index.tcl | 422 ++++++++++++++++++++++++++++++++++++++++----------\n 3 files changed, 502 insertions(+), 84 deletions(-)\n create mode 100644 lib/chord.tcl\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex e317f528af..6dcf6551b6 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]\n ##\n ## Tcl/Tk sanity check\n \n-if {[catch {package require Tcl 8.4} err]\n- || [catch {package require Tk  8.4} err]\n+if {[catch {package require Tcl 8.6} err]\n+ || [catch {package require Tk  8.6} err]\n } {\n \tcatch {wm withdraw .}\n \ttk_messageBox \\\ndiff --git a/lib/chord.tcl b/lib/chord.tcl\nnew file mode 100644\nindex 0000000000..275a6cd4a1\n--- /dev/null\n+++ b/lib/chord.tcl\n@@ -0,0 +1,160 @@\n+# Simple Chord for Tcl\n+#\n+# A \"chord\" is a method with more than one entrypoint and only one body, such\n+# that the body runs only once all the entrypoints have been called by\n+# different asynchronous tasks. In this implementation, the chord is defined\n+# dynamically for each invocation. A SimpleChord object is created, supplying\n+# body script to be run when the chord is completed, and then one or more notes\n+# are added to the chord. Each note can be called like a proc, and returns\n+# immediately if the chord isn't yet complete. When the last remaining note is\n+# called, the body runs before the note returns.\n+#\n+# The SimpleChord class has a constructor that takes the body script, and a\n+# method add_note that returns a note object. Since the body script does not\n+# run in the context of the procedure that defined it, a mechanism is provided\n+# for injecting variables into the chord for use by the body script. The\n+# activation of a note is idempotent; multiple calls have the same effect as\n+# a simple call.\n+#\n+# If you are invoking asynchronous operations with chord notes as completion\n+# callbacks, and there is a possibility that earlier operations could complete\n+# before later ones are started, it is a good practice to create a \"common\"\n+# note on the chord that prevents it from being complete until you're certain\n+# you've added all the notes you need.\n+#\n+# Example:\n+#\n+#   # Turn off the UI while running a couple of async operations.\n+#   lock_ui\n+#\n+#   set chord [SimpleChord new {\n+#     unlock_ui\n+#     # Note: $notice here is not referenced in the calling scope\n+#     if {$notice} { info_popup $notice }\n+#   }\n+#\n+#   # Configure a note to keep the chord from completing until\n+#   # all operations have been initiated.\n+#   set common_note [$chord add_note]\n+#\n+#   # Pass notes as 'after' callbacks to other operations\n+#   async_operation $args [$chord add_note]\n+#   other_async_operation $args [$chord add_note]\n+#\n+#   # Communicate with the chord body\n+#   if {$condition} {\n+#     # This sets $notice in the same context that the chord body runs in.\n+#     $chord eval { set notice \"Something interesting\" }\n+#   }\n+#\n+#   # Activate the common note, making the chord eligible to complete\n+#   $common_note\n+#\n+# At this point, the chord will complete at some unknown point in the future.\n+# The common note might have been the first note activated, or the async\n+# operations might have completed synchronously and the common note is the\n+# last one, completing the chord before this code finishes, or anything in\n+# between. The purpose of the chord is to not have to worry about the order.\n+\n+# SimpleChord class:\n+#   Represents a procedure that conceptually has multiple entrypoints that must\n+#   all be called before the procedure executes. Each entrypoint is called a\n+#   \"note\". The chord is only \"completed\" when all the notes are \"activated\".\n+oo::class create SimpleChord {\n+\tvariable notes body is_completed\n+\n+\t# Constructor:\n+\t#   set chord [SimpleChord new {body}]\n+\t#     Creates a new chord object with the specified body script. The\n+\t#     body script is evaluated at most once, when a note is activated\n+\t#     and the chord has no other non-activated notes.\n+\tconstructor {body} {\n+\t\tset notes [list]\n+\t\tmy eval [list set body $body]\n+\t\tset is_completed 0\n+\t}\n+\n+\t# Method:\n+\t#   $chord eval {script}\n+\t#     Runs the specified script in the same context (namespace) in which\n+\t#     the chord body will be evaluated. This can be used to set variable\n+\t#     values for the chord body to use.\n+\tmethod eval {script} {\n+\t\tnamespace eval [self] $script\n+\t}\n+\n+\t# Method:\n+\t#   set note [$chord add_note]\n+\t#     Adds a new note to the chord, an instance of ChordNote. Raises an\n+\t#     error if the chord is already completed, otherwise the chord is\n+\t#     updated so that the new note must also be activated before the\n+\t#     body is evaluated.\n+\tmethod add_note {} {\n+\t\tif {$is_completed} { error \"Cannot add a note to a completed chord\" }\n+\n+\t\tset note [ChordNote new [self]]\n+\n+\t\tlappend notes $note\n+\n+\t\treturn $note\n+\t}\n+\n+\t# This method is for internal use only and is intentionally undocumented.\n+\tmethod notify_note_activation {} {\n+\t\tif {!$is_completed} {\n+\t\t\tforeach note $notes {\n+\t\t\t\tif {![$note is_activated]} { return }\n+\t\t\t}\n+\n+\t\t\tset is_completed 1\n+\n+\t\t\tnamespace eval [self] $body\n+\t\t\tnamespace delete [self]\n+\t\t}\n+\t}\n+}\n+\n+# ChordNote class:\n+#   Represents a note within a chord, providing a way to activate it. When the\n+#   final note of the chord is activated (this can be any note in the chord,\n+#   with all other notes already previously activated in any order), the chord's\n+#   body is evaluated.\n+oo::class create ChordNote {\n+\tvariable chord is_activated\n+\n+\t# Constructor:\n+\t#   Instances of ChordNote are created internally by calling add_note on\n+\t#   SimpleChord objects.\n+\tconstructor {chord} {\n+\t\tmy eval set chord $chord\n+\t\tset is_activated 0\n+\t}\n+\n+\t# Method:\n+\t#   [$note is_activated]\n+\t#     Returns true if this note has already been activated.\n+\tmethod is_activated {} {\n+\t\treturn $is_activated\n+\t}\n+\n+\t# Method:\n+\t#   $note\n+\t#     Activates the note, if it has not already been activated, and\n+\t#     completes the chord if there are no other notes awaiting\n+\t#     activation. Subsequent calls will have no further effect.\n+\t#\n+\t# NB: In TclOO, if an object is invoked like a method without supplying\n+\t#     any method name, then this internal method `unknown` is what\n+\t#     actually runs (with no parameters). It is used in the ChordNote\n+\t#     class for the purpose of allowing the note object to be called as\n+\t#     a function (see example above). (The `unknown` method can also be\n+\t#     used to support dynamic dispatch, but must take parameters to\n+\t#     identify the \"unknown\" method to be invoked. In this form, this\n+\t#     proc serves only to make instances behave directly like methods.)\n+\tmethod unknown {} {\n+\t\tif {!$is_activated} {\n+\t\t\tset is_activated 1\n+\t\t\t$chord notify_note_activation\n+\t\t}\n+\t}\n+}\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 62f4773ef4..1254145634 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -7,53 +7,60 @@ proc _delete_indexlock {} {\n \t}\n }\n \n-proc _close_updateindex {fd after} {\n-\tglobal use_ttk NS\n-\tfconfigure $fd -blocking 1\n-\tif {[catch {close $fd} err]} {\n-\t\tset w .indexfried\n-\t\tDialog $w\n-\t\twm withdraw $w\n-\t\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n-\t\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n-\t\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n-\t\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n-\t\t\t-width [string length $s] -relief flat \\\n-\t\t\t-borderwidth 0 -highlightthickness 0 \\\n-\t\t\t-background [get_bg_color $w]\n-\t\t$w.msg tag configure bold -font font_uibold -justify center\n-\t\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n-\t\t$w.msg insert end $s bold \\n\\n$err {}\n-\t\t$w.msg configure -state disabled\n-\n-\t\t${NS}::button $w.continue \\\n-\t\t\t-text [mc \"Continue\"] \\\n-\t\t\t-command [list destroy $w]\n-\t\t${NS}::button $w.unlock \\\n-\t\t\t-text [mc \"Unlock Index\"] \\\n-\t\t\t-command \"destroy $w; _delete_indexlock\"\n-\t\tgrid $w.msg - $w.vs -sticky news\n-\t\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n-\t\tgrid columnconfigure $w 0 -weight 1\n-\t\tgrid rowconfigure $w 0 -weight 1\n-\n-\t\twm protocol $w WM_DELETE_WINDOW update\n-\t\tbind $w.continue <Visibility> \"\n-\t\t\tgrab $w\n-\t\t\tfocus %W\n-\t\t\"\n-\t\twm deiconify $w\n-\t\ttkwait window $w\n-\n-\t\t$::main_status stop_all\n+proc close_and_unlock_index {fd after} {\n+\tif {![catch {_close_updateindex $fd} err]} {\n \t\tunlock_index\n-\t\trescan $after 0\n-\t\treturn\n+\t\tuplevel #0 $after\n+\t} else {\n+\t\trescan_on_error $err $after\n \t}\n+}\n+\n+proc _close_updateindex {fd} {\n+\tfconfigure $fd -blocking 1\n+\tclose $fd\n+}\n+\n+proc rescan_on_error {err {after {}}} {\n+\tglobal use_ttk NS\n+\n+\tset w .indexfried\n+\tDialog $w\n+\twm withdraw $w\n+\twm title $w [strcat \"[appname] ([reponame]): \" [mc \"Index Error\"]]\n+\twm geometry $w \"+[winfo rootx .]+[winfo rooty .]\"\n+\tset s [mc \"Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui.\"]\n+\ttext $w.msg -yscrollcommand [list $w.vs set] \\\n+\t\t-width [string length $s] -relief flat \\\n+\t\t-borderwidth 0 -highlightthickness 0 \\\n+\t\t-background [get_bg_color $w]\n+\t$w.msg tag configure bold -font font_uibold -justify center\n+\t${NS}::scrollbar $w.vs -command [list $w.msg yview]\n+\t$w.msg insert end $s bold \\n\\n$err {}\n+\t$w.msg configure -state disabled\n+\n+\t${NS}::button $w.continue \\\n+\t\t-text [mc \"Continue\"] \\\n+\t\t-command [list destroy $w]\n+\t${NS}::button $w.unlock \\\n+\t\t-text [mc \"Unlock Index\"] \\\n+\t\t-command \"destroy $w; _delete_indexlock\"\n+\tgrid $w.msg - $w.vs -sticky news\n+\tgrid $w.unlock $w.continue - -sticky se -padx 2 -pady 2\n+\tgrid columnconfigure $w 0 -weight 1\n+\tgrid rowconfigure $w 0 -weight 1\n+\n+\twm protocol $w WM_DELETE_WINDOW update\n+\tbind $w.continue <Visibility> \"\n+\t\tgrab $w\n+\t\tfocus %W\n+\t\"\n+\twm deiconify $w\n+\ttkwait window $w\n \n \t$::main_status stop_all\n \tunlock_index\n-\tuplevel #0 $after\n+\trescan [concat $after [list ui_ready]] 0\n }\n \n proc update_indexinfo {msg path_list after} {\n@@ -93,7 +100,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -162,7 +169,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\t\tclose_and_unlock_index $fd $after\n \t\treturn\n \t}\n \n@@ -199,7 +206,7 @@ proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n \t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n-proc checkout_index {msg path_list after} {\n+proc checkout_index {msg path_list after capture_error} {\n \tglobal update_index_cp\n \n \tif {![lock_index update]} return\n@@ -232,17 +239,33 @@ proc checkout_index {msg path_list after} {\n \t\t$batch \\\n \t\t$status_bar_operation \\\n \t\t$after \\\n+\t\t$capture_error \\\n \t\t]\n }\n \n proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n-\tafter} {\n+\tafter capture_error} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n \t\t$status_bar_operation stop\n-\t\t_close_updateindex $fd $after\n+\n+\t\t# We do not unlock the index directly here because this\n+\t\t# operation expects to potentially run in parallel with file\n+\t\t# deletions scheduled by revert_helper. We're done with the\n+\t\t# update index, so we close it, but actually unlocking the index\n+\t\t# and dealing with potential errors is deferred to the chord\n+\t\t# body that runs when all async operations are completed.\n+\t\t#\n+\t\t# (See after_chord in revert_helper.)\n+\n+\t\tif {[catch {_close_updateindex $fd} err]} {\n+\t\t\tuplevel #0 $capture_error [list $err]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\n \t\treturn\n \t}\n \n@@ -397,66 +420,301 @@ proc do_add_all {} {\n \tadd_helper [mc \"Adding all changed files\"] $paths\n }\n \n+# Copied from TclLib package \"lambda\".\n+proc lambda {arguments body args} {\n+\treturn [list ::apply [list $arguments $body] {*}$args]\n+}\n+\n proc revert_helper {txt paths} {\n \tglobal file_states current_diff_path\n \n \tif {![lock_index begin-update]} return\n \n+\t# Common \"after\" functionality that waits until multiple asynchronous\n+\t# operations are complete (by waiting for them to activate their notes\n+\t# on the chord).\n+\t#\n+\t# The asynchronous operations are each indicated below by a comment\n+\t# before the code block that starts the async operation.\n+\tset after_chord [SimpleChord new {\n+\t\tif {[string trim $err] != \"\"} {\n+\t\t\trescan_on_error $err\n+\t\t} else {\n+\t\t\tunlock_index\n+\t\t\tif {$should_reshow_diff} { reshow_diff }\n+\t\t\tui_ready\n+\t\t}\n+\t}]\n+\n+\t$after_chord eval { set should_reshow_diff 0 }\n+\n+\t# This function captures an error for processing when after_chord is\n+\t# completed. (The chord is curried into the lambda function.)\n+\tset capture_error [lambda \\\n+\t\t{chord error} \\\n+\t\t{ $chord eval [list set err $error] } \\\n+\t\t$after_chord]\n+\n+\t# We don't know how many notes we're going to create (it's dynamic based\n+\t# on conditional paths below), so create a common note that will delay\n+\t# the chord's completion until we activate it, and then activate it\n+\t# after all the other notes have been created.\n+\tset after_common_note [$after_chord add_note]\n+\n \tset path_list [list]\n-\tset after {}\n+\tset untracked_list [list]\n+\n \tforeach path $paths {\n \t\tswitch -glob -- [lindex $file_states($path) 0] {\n \t\tU? {continue}\n+\t\t?O {\n+\t\t\tlappend untracked_list $path\n+\t\t}\n \t\t?M -\n \t\t?T -\n \t\t?D {\n \t\t\tlappend path_list $path\n \t\t\tif {$path eq $current_diff_path} {\n-\t\t\t\tset after {reshow_diff;}\n+\t\t\t\t$after_chord eval { set should_reshow_diff 1 }\n \t\t\t}\n \t\t}\n \t\t}\n \t}\n \n+\tset path_cnt [llength $path_list]\n+\tset untracked_cnt [llength $untracked_list]\n+\n+\t# Asynchronous operation: revert changes by checking them out afresh\n+\t# from the index.\n+\tif {$path_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages. Previously, the\n+\t\t# code used \"Revert changes in\" for both, but that can't work\n+\t\t# in languages where 'in' must be combined with word from\n+\t\t# rest of string (in different way for both cases of course).\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$path_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in file %s?\" \\\n+\t\t\t\t[short_path [lindex $path_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Revert changes in these %i files?\" \\\n+\t\t\t\t$path_cnt]\n+\t\t}\n \n-\t# Split question between singular and plural cases, because\n-\t# such distinction is needed in some languages. Previously, the\n-\t# code used \"Revert changes in\" for both, but that can't work\n-\t# in languages where 'in' must be combined with word from\n-\t# rest of string (in different way for both cases of course).\n-\t#\n-\t# FIXME: Unfortunately, even that isn't enough in some languages\n-\t# as they have quite complex plural-form rules. Unfortunately,\n-\t# msgcat doesn't seem to support that kind of string translation.\n-\t#\n-\tset n [llength $path_list]\n-\tif {$n == 0} {\n-\t\tunlock_index\n-\t\treturn\n-\t} elseif {$n == 1} {\n-\t\tset query [mc \"Revert changes in file %s?\" [short_path [lindex $path_list]]]\n-\t} else {\n-\t\tset query [mc \"Revert changes in these %i files?\" $n]\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Revert Changes\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\tcheckout_index \\\n+\t\t\t\t$txt \\\n+\t\t\t\t$path_list \\\n+\t\t\t\t[$after_chord add_note] \\\n+\t\t\t\t$capture_error\n+\t\t}\n \t}\n \n-\tset reply [tk_dialog \\\n-\t\t.confirm_revert \\\n-\t\t\"[appname] ([reponame])\" \\\n-\t\t\"$query\n+\t# Asynchronous operation: Deletion of untracked files.\n+\tif {$untracked_cnt > 0} {\n+\t\t# Split question between singular and plural cases, because\n+\t\t# such distinction is needed in some languages.\n+\t\t#\n+\t\t# FIXME: Unfortunately, even that isn't enough in some languages\n+\t\t# as they have quite complex plural-form rules. Unfortunately,\n+\t\t# msgcat doesn't seem to support that kind of string\n+\t\t# translation.\n+\t\t#\n+\t\tif {$untracked_cnt == 1} {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete untracked file %s?\" \\\n+\t\t\t\t[short_path [lindex $untracked_list]] \\\n+\t\t\t\t]\n+\t\t} else {\n+\t\t\tset query [mc \\\n+\t\t\t\t\"Delete these %i untracked files?\" \\\n+\t\t\t\t$untracked_cnt \\\n+\t\t\t\t]\n+\t\t}\n \n-[mc \"Any unstaged changes will be permanently lost by the revert.\"]\" \\\n-\t\tquestion \\\n-\t\t1 \\\n-\t\t[mc \"Do Nothing\"] \\\n-\t\t[mc \"Revert Changes\"] \\\n-\t\t]\n-\tif {$reply == 1} {\n-\t\tcheckout_index \\\n-\t\t\t$txt \\\n+\t\tset reply [tk_dialog \\\n+\t\t\t.confirm_revert \\\n+\t\t\t\"[appname] ([reponame])\" \\\n+\t\t\t\"$query\n+\n+[mc \"Files will be permanently deleted.\"]\" \\\n+\t\t\tquestion \\\n+\t\t\t1 \\\n+\t\t\t[mc \"Do Nothing\"] \\\n+\t\t\t[mc \"Delete Files\"] \\\n+\t\t\t]\n+\n+\t\tif {$reply == 1} {\n+\t\t\t$after_chord eval { set should_reshow_diff 1 }\n+\n+\t\t\tdelete_files $untracked_list [$after_chord add_note]\n+\t\t}\n+\t}\n+\n+\t# Activate the common note. If no other notes were created, this\n+\t# completes the chord. If other notes were created, then this common\n+\t# note prevents a race condition where the chord might complete early.\n+\t$after_common_note\n+}\n+\n+# Delete all of the specified files, performing deletion in batches to allow the\n+# UI to remain responsive and updated.\n+proc delete_files {path_list after} {\n+\t# Enable progress bar status updates\n+\tset status_bar_operation [$::main_status \\\n+\t\tstart \\\n+\t\t[mc \"Deleting\"] \\\n+\t\t[mc \"files\"]]\n+\n+\tset path_index 0\n+\tset deletion_errors [list]\n+\tset batch_size 50\n+\n+\tdelete_helper \\\n+\t\t$path_list \\\n+\t\t$path_index \\\n+\t\t$deletion_errors \\\n+\t\t$batch_size \\\n+\t\t$status_bar_operation \\\n+\t\t$after\n+}\n+\n+# Helper function to delete a list of files in batches. Each call deletes one\n+# batch of files, and then schedules a call for the next batch after any UI\n+# messages have been processed.\n+proc delete_helper {path_list path_index deletion_errors batch_size \\\n+\tstatus_bar_operation after} {\n+\tglobal file_states\n+\n+\tset path_cnt [llength $path_list]\n+\n+\tset batch_remaining $batch_size\n+\n+\twhile {$batch_remaining > 0} {\n+\t\tif {$path_index >= $path_cnt} { break }\n+\n+\t\tset path [lindex $path_list $path_index]\n+\n+\t\tset deletion_failed [catch {file delete -- $path} deletion_error]\n+\n+\t\tif {$deletion_failed} {\n+\t\t\tlappend deletion_errors [list \"$deletion_error\"]\n+\t\t} else {\n+\t\t\tremove_empty_directories [file dirname $path]\n+\n+\t\t\t# Don't assume the deletion worked. Remove the file from\n+\t\t\t# the UI, but only if it no longer exists.\n+\t\t\tif {![path_exists $path]} {\n+\t\t\t\tunset file_states($path)\n+\t\t\t\tdisplay_file $path __\n+\t\t\t}\n+\t\t}\n+\n+\t\tincr path_index 1\n+\t\tincr batch_remaining -1\n+\t}\n+\n+\t# Update the progress bar to indicate that this batch has been\n+\t# completed. The update will be visible when this procedure returns\n+\t# and allows the UI thread to process messages.\n+\t$status_bar_operation update $path_index $path_cnt\n+\n+\tif {$path_index < $path_cnt} {\n+\t\t# The Tcler's Wiki lists this as the best practice for keeping\n+\t\t# a UI active and processing messages during a long-running\n+\t\t# operation.\n+\n+\t\tafter idle [list after 0 [list \\\n+\t\t\tdelete_helper \\\n \t\t\t$path_list \\\n-\t\t\t[concat $after [list ui_ready]]\n+\t\t\t$path_index \\\n+\t\t\t$deletion_errors \\\n+\t\t\t$batch_size \\\n+\t\t\t$status_bar_operation \\\n+\t\t\t$after\n+\t\t\t]]\n \t} else {\n-\t\tunlock_index\n+\t\t# Finish the status bar operation.\n+\t\t$status_bar_operation stop\n+\n+\t\t# Report error, if any, based on how many deletions failed.\n+\t\tset deletion_error_cnt [llength $deletion_errors]\n+\n+\t\tif {($deletion_error_cnt > 0)\n+\t\t && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} {\n+\t\t\tset error_text [mc \"Encountered errors deleting files:\\n\"]\n+\n+\t\t\tforeach deletion_error $deletion_errors {\n+\t\t\t\tappend error_text \"* [lindex $deletion_error 0]\\n\"\n+\t\t\t}\n+\n+\t\t\terror_popup $error_text\n+\t\t} elseif {$deletion_error_cnt == $path_cnt} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"None of the %d selected files could be deleted.\" \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t} elseif {$deletion_error_cnt > 1} {\n+\t\t\terror_popup [mc \\\n+\t\t\t\t\"%d of the %d selected files could not be deleted.\" \\\n+\t\t\t\t$deletion_error_cnt \\\n+\t\t\t\t$path_cnt \\\n+\t\t\t\t]\n+\t\t}\n+\n+\t\tuplevel #0 $after\n+\t}\n+}\n+\n+proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }\n+\n+# This function is from the TCL documentation:\n+#\n+#   https://wiki.tcl-lang.org/page/file+exists\n+#\n+# [file exists] returns false if the path does exist but is a symlink to a path\n+# that doesn't exist. This proc returns true if the path exists, regardless of\n+# whether it is a symlink and whether it is broken.\n+proc path_exists {name} {\n+\texpr {![catch {file lstat $name finfo}]}\n+}\n+\n+# Remove as many empty directories as we can starting at the specified path,\n+# walking up the directory tree. If we encounter a directory that is not\n+# empty, or if a directory deletion fails, then we stop the operation and\n+# return to the caller. Even if this procedure fails to delete any\n+# directories at all, it does not report failure.\n+proc remove_empty_directories {directory_path} {\n+\tset parent_path [file dirname $directory_path]\n+\n+\twhile {$parent_path != $directory_path} {\n+\t\tset contents [glob -nocomplain -dir $directory_path *]\n+\n+\t\tif {[llength $contents] > 0} { break }\n+\t\tif {[catch {file delete -- $directory_path}]} { break }\n+\n+\t\tset directory_path $parent_path\n+\t\tset parent_path [file dirname $directory_path]\n \t}\n }\n \n-- \ngitgitgadget\n"},{"id":"387366","messageId":"8fe9dfc30771574344c1a2fc5cd8f21f1a617388.1575167313.git.gitgitgadget@gmail.com","threadId":"52154","inReplyTo":"pull.436.v7.git.1575167313.gitgitgadget@gmail.com","subject":"[PATCH v7 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-01T02:28:32Z","receivedAt":"2019-12-01T02:28:42Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n\nUpdate the status bar to track updates as individual \"operations\" that\ncan overlap. Update all call sites to interact with the new status bar\nmechanism. Update initialization to explicitly clear status text,\nsince otherwise it may persist across future operations.\n\nSigned-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n---\n git-gui.sh                |  27 +++--\n lib/blame.tcl             |  24 ++--\n lib/checkout_op.tcl       |  15 +--\n lib/choose_repository.tcl | 120 +++++++++++++-------\n lib/index.tcl             |  31 +++--\n lib/merge.tcl             |  14 ++-\n lib/status_bar.tcl        | 231 +++++++++++++++++++++++++++++++++-----\n 7 files changed, 354 insertions(+), 108 deletions(-)\n\ndiff --git a/git-gui.sh b/git-gui.sh\nindex 0d21f5688b..e317f528af 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -1797,10 +1797,10 @@ proc ui_status {msg} {\n \t}\n }\n \n-proc ui_ready {{test {}}} {\n+proc ui_ready {} {\n \tglobal main_status\n \tif {[info exists main_status]} {\n-\t\t$main_status show [mc \"Ready.\"] $test\n+\t\t$main_status show [mc \"Ready.\"]\n \t}\n }\n \n@@ -2150,8 +2150,6 @@ proc incr_font_size {font {amt 1}} {\n ##\n ## ui commands\n \n-set starting_gitk_msg [mc \"Starting gitk... please wait...\"]\n-\n proc do_gitk {revs {is_submodule false}} {\n \tglobal current_diff_path file_states current_diff_side ui_index\n \tglobal _gitdir _gitworktree\n@@ -2206,10 +2204,11 @@ proc do_gitk {revs {is_submodule false}} {\n \t\tset env(GIT_WORK_TREE) $_gitworktree\n \t\tcd $pwd\n \n-\t\tui_status $::starting_gitk_msg\n-\t\tafter 10000 {\n-\t\t\tui_ready $starting_gitk_msg\n-\t\t}\n+\t\tset status_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n+\n+\t\tafter 3500 [list $status_operation stop]\n \t}\n }\n \n@@ -2240,10 +2239,11 @@ proc do_git_gui {} {\n \t\tset env(GIT_WORK_TREE) $_gitworktree\n \t\tcd $pwd\n \n-\t\tui_status $::starting_gitk_msg\n-\t\tafter 10000 {\n-\t\t\tui_ready $starting_gitk_msg\n-\t\t}\n+\t\tset status_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Starting %s... please wait...\" \"git-gui\"]]\n+\n+\t\tafter 3500 [list $status_operation stop]\n \t}\n }\n \n@@ -4159,6 +4159,9 @@ if {$picked && [is_config_true gui.autoexplore]} {\n \tdo_explore\n }\n \n+# Clear \"Initializing...\" status\n+after 500 {$main_status show \"\"}\n+\n # Local variables:\n # mode: tcl\n # indent-tabs-mode: t\ndiff --git a/lib/blame.tcl b/lib/blame.tcl\nindex a1aeb8b96e..62ec083667 100644\n--- a/lib/blame.tcl\n+++ b/lib/blame.tcl\n@@ -24,6 +24,7 @@ field w_cviewer  ; # pane showing commit message\n field finder     ; # find mini-dialog frame\n field gotoline   ; # line goto mini-dialog frame\n field status     ; # status mega-widget instance\n+field status_operation ; # operation displayed by status mega-widget\n field old_height ; # last known height of $w.file_pane\n \n \n@@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {\n \tpack $w_cviewer -expand 1 -fill both\n \n \tset status [::status_bar::new $w.status]\n+\tset status_operation {}\n \n \tmenu $w.ctxm -tearoff 0\n \t$w.ctxm add command \\\n@@ -602,16 +604,23 @@ method _exec_blame {cur_w cur_d options cur_s} {\n \t} else {\n \t\tlappend options $commit\n \t}\n+\n+\t# We may recurse in from another call to _exec_blame and already have\n+\t# a status operation.\n+\tif {$status_operation == {}} {\n+\t\tset status_operation [$status start \\\n+\t\t\t$cur_s \\\n+\t\t\t[mc \"lines annotated\"]]\n+\t} else {\n+\t\t$status_operation restart $cur_s\n+\t}\n+\n \tlappend options -- $path\n \tset fd [eval git_read --nice blame $options]\n \tfconfigure $fd -blocking 0 -translation lf -encoding utf-8\n \tfileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]\n \tset current_fd $fd\n \tset blame_lines 0\n-\n-\t$status start \\\n-\t\t$cur_s \\\n-\t\t[mc \"lines annotated\"]\n }\n \n method _read_blame {fd cur_w cur_d} {\n@@ -806,10 +815,11 @@ method _read_blame {fd cur_w cur_d} {\n \t\t\t\t[mc \"Loading original location annotations...\"]\n \t\t} else {\n \t\t\tset current_fd {}\n-\t\t\t$status stop [mc \"Annotation complete.\"]\n+\t\t\t$status_operation stop [mc \"Annotation complete.\"]\n+\t\t\tset status_operation {}\n \t\t}\n \t} else {\n-\t\t$status update $blame_lines $total_lines\n+\t\t$status_operation update $blame_lines $total_lines\n \t}\n } ifdeleted { catch {close $fd} }\n \n@@ -1124,7 +1134,7 @@ method _blameparent {} {\n \t\t\tset diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]\n \t\t}\n \t\tif {[catch {set fd [eval git_read $diffcmd]} err]} {\n-\t\t\t$status stop [mc \"Unable to display parent\"]\n+\t\t\t$status_operation stop [mc \"Unable to display parent\"]\n \t\t\terror_popup [strcat [mc \"Error loading diff:\"] \"\\n\\n$err\"]\n \t\t\treturn\n \t\t}\ndiff --git a/lib/checkout_op.tcl b/lib/checkout_op.tcl\nindex a5228297db..21ea768d80 100644\n--- a/lib/checkout_op.tcl\n+++ b/lib/checkout_op.tcl\n@@ -341,9 +341,9 @@ method _readtree {} {\n \tglobal HEAD\n \n \tset readtree_d {}\n-\t$::main_status start \\\n+\tset status_bar_operation [$::main_status start \\\n \t\t[mc \"Updating working directory to '%s'...\" [_name $this]] \\\n-\t\t[mc \"files checked out\"]\n+\t\t[mc \"files checked out\"]]\n \n \tset fd [git_read --stderr read-tree \\\n \t\t-m \\\n@@ -354,26 +354,27 @@ method _readtree {} {\n \t\t$new_hash \\\n \t\t]\n \tfconfigure $fd -blocking 0 -translation binary\n-\tfileevent $fd readable [cb _readtree_wait $fd]\n+\tfileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]\n }\n \n-method _readtree_wait {fd} {\n+method _readtree_wait {fd status_bar_operation} {\n \tglobal current_branch\n \n \tset buf [read $fd]\n-\t$::main_status update_meter $buf\n+\t$status_bar_operation update_meter $buf\n \tappend readtree_d $buf\n \n \tfconfigure $fd -blocking 1\n \tif {![eof $fd]} {\n \t\tfconfigure $fd -blocking 0\n+\t\t$status_bar_operation stop\n \t\treturn\n \t}\n \n \tif {[catch {close $fd}]} {\n \t\tset err $readtree_d\n \t\tregsub {^fatal: } $err {} err\n-\t\t$::main_status stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n+\t\t$status_bar_operation stop [mc \"Aborted checkout of '%s' (file level merging is required).\" [_name $this]]\n \t\twarn_popup [strcat [mc \"File level merge required.\"] \"\n \n $err\n@@ -384,7 +385,7 @@ $err\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$status_bar_operation stop\n \t_after_readtree $this\n }\n \ndiff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl\nindex 80f5a59bbb..e54f3e66d8 100644\n--- a/lib/choose_repository.tcl\n+++ b/lib/choose_repository.tcl\n@@ -9,6 +9,18 @@ field w_body      ; # Widget holding the center content\n field w_next      ; # Next button\n field w_quit      ; # Quit button\n field o_cons      ; # Console object (if active)\n+\n+# Status mega-widget instance during _do_clone2 (used by _copy_files and\n+# _link_files). Widget is destroyed before _do_clone2 calls\n+# _do_clone_checkout\n+field o_status\n+\n+# Operation displayed by status mega-widget during _do_clone_checkout =>\n+# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n+# _do_validate_submodule_cloning. The status mega-widget is a different\n+# instance than that stored in $o_status in earlier operations.\n+field o_status_op\n+\n field w_types     ; # List of type buttons in clone\n field w_recentlist ; # Listbox containing recent repositories\n field w_localpath  ; # Entry widget bound to local_path\n@@ -659,12 +671,12 @@ method _do_clone2 {} {\n \n \tswitch -exact -- $clone_type {\n \thardlink {\n-\t\tset o_cons [status_bar::two_line $w_body]\n+\t\tset o_status [status_bar::two_line $w_body]\n \t\tpack $w_body -fill x -padx 10 -pady 10\n \n-\t\t$o_cons start \\\n+\t\tset status_op [$o_status start \\\n \t\t\t[mc \"Counting objects\"] \\\n-\t\t\t[mc \"buckets\"]\n+\t\t\t[mc \"buckets\"]]\n \t\tupdate\n \n \t\tif {[file exists [file join $objdir info alternates]]} {\n@@ -689,6 +701,7 @@ method _do_clone2 {} {\n \t\t\t} err]} {\n \t\t\t\tcatch {cd $pwd}\n \t\t\t\t_clone_failed $this [mc \"Unable to copy objects/info/alternates: %s\" $err]\n+\t\t\t\t$status_op stop\n \t\t\t\treturn\n \t\t\t}\n \t\t}\n@@ -700,7 +713,7 @@ method _do_clone2 {} {\n \t\t\t-directory [file join $objdir] ??]\n \t\tset bcnt [expr {[llength $buckets] + 2}]\n \t\tset bcur 1\n-\t\t$o_cons update $bcur $bcnt\n+\t\t$status_op update $bcur $bcnt\n \t\tupdate\n \n \t\tfile mkdir [file join .git objects pack]\n@@ -708,7 +721,7 @@ method _do_clone2 {} {\n \t\t\t-directory [file join $objdir pack] *] {\n \t\t\tlappend tolink [file join pack $i]\n \t\t}\n-\t\t$o_cons update [incr bcur] $bcnt\n+\t\t$status_op update [incr bcur] $bcnt\n \t\tupdate\n \n \t\tforeach i $buckets {\n@@ -717,10 +730,10 @@ method _do_clone2 {} {\n \t\t\t\t-directory [file join $objdir $i] *] {\n \t\t\t\tlappend tolink [file join $i $j]\n \t\t\t}\n-\t\t\t$o_cons update [incr bcur] $bcnt\n+\t\t\t$status_op update [incr bcur] $bcnt\n \t\t\tupdate\n \t\t}\n-\t\t$o_cons stop\n+\t\t$status_op stop\n \n \t\tif {$tolink eq {}} {\n \t\t\tinfo_popup [strcat \\\n@@ -747,6 +760,8 @@ method _do_clone2 {} {\n \t\tif {!$i} return\n \n \t\tdestroy $w_body\n+\n+\t\tset o_status {}\n \t}\n \tfull {\n \t\tset o_cons [console::embed \\\n@@ -781,9 +796,9 @@ method _do_clone2 {} {\n }\n \n method _copy_files {objdir tocopy} {\n-\t$o_cons start \\\n+\tset status_op [$o_status start \\\n \t\t[mc \"Copying objects\"] \\\n-\t\t[mc \"KiB\"]\n+\t\t[mc \"KiB\"]]\n \tset tot 0\n \tset cmp 0\n \tforeach p $tocopy {\n@@ -798,7 +813,7 @@ method _copy_files {objdir tocopy} {\n \n \t\t\t\twhile {![eof $f_in]} {\n \t\t\t\t\tincr cmp [fcopy $f_in $f_cp -size 16384]\n-\t\t\t\t\t$o_cons update \\\n+\t\t\t\t\t$status_op update \\\n \t\t\t\t\t\t[expr {$cmp / 1024}] \\\n \t\t\t\t\t\t[expr {$tot / 1024}]\n \t\t\t\t\tupdate\n@@ -808,17 +823,19 @@ method _copy_files {objdir tocopy} {\n \t\t\t\tclose $f_cp\n \t\t\t} err]} {\n \t\t\t_clone_failed $this [mc \"Unable to copy object: %s\" $err]\n+\t\t\t$status_op stop\n \t\t\treturn 0\n \t\t}\n \t}\n+\t$status_op stop\n \treturn 1\n }\n \n method _link_files {objdir tolink} {\n \tset total [llength $tolink]\n-\t$o_cons start \\\n+\tset status_op [$o_status start \\\n \t\t[mc \"Linking objects\"] \\\n-\t\t[mc \"objects\"]\n+\t\t[mc \"objects\"]]\n \tfor {set i 0} {$i < $total} {} {\n \t\tset p [lindex $tolink $i]\n \t\tif {[catch {\n@@ -827,15 +844,17 @@ method _link_files {objdir tolink} {\n \t\t\t\t\t[file join $objdir $p]\n \t\t\t} err]} {\n \t\t\t_clone_failed $this [mc \"Unable to hardlink object: %s\" $err]\n+\t\t\t$status_op stop\n \t\t\treturn 0\n \t\t}\n \n \t\tincr i\n \t\tif {$i % 5 == 0} {\n-\t\t\t$o_cons update $i $total\n+\t\t\t$status_op update $i $total\n \t\t\tupdate\n \t\t}\n \t}\n+\t$status_op stop\n \treturn 1\n }\n \n@@ -958,11 +977,26 @@ method _do_clone_checkout {HEAD} {\n \t\treturn\n \t}\n \n-\tset o_cons [status_bar::two_line $w_body]\n+\tset status [status_bar::two_line $w_body]\n \tpack $w_body -fill x -padx 10 -pady 10\n-\t$o_cons start \\\n+\n+\t# We start the status operation here.\n+\t#\n+\t# This function calls _readtree_wait as a callback.\n+\t#\n+\t# _readtree_wait in turn either calls _do_clone_submodules directly,\n+\t# or calls _postcheckout_wait as a callback which then calls\n+\t# _do_clone_submodules.\n+\t#\n+\t# _do_clone_submodules calls _do_validate_submodule_cloning.\n+\t#\n+\t# _do_validate_submodule_cloning stops the status operation.\n+\t#\n+\t# There are no other calls into this chain from other code.\n+\n+\tset o_status_op [$status start \\\n \t\t[mc \"Creating working directory\"] \\\n-\t\t[mc \"files\"]\n+\t\t[mc \"files\"]]\n \n \tset readtree_err {}\n \tset fd [git_read --stderr read-tree \\\n@@ -976,33 +1010,9 @@ method _do_clone_checkout {HEAD} {\n \tfileevent $fd readable [cb _readtree_wait $fd]\n }\n \n-method _do_validate_submodule_cloning {ok} {\n-\tif {$ok} {\n-\t\t$o_cons done $ok\n-\t\tset done 1\n-\t} else {\n-\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n-\t}\n-}\n-\n-method _do_clone_submodules {} {\n-\tif {$recursive eq {true}} {\n-\t\tdestroy $w_body\n-\t\tset o_cons [console::embed \\\n-\t\t\t$w_body \\\n-\t\t\t[mc \"Cloning submodules\"]]\n-\t\tpack $w_body -fill both -expand 1 -padx 10\n-\t\t$o_cons exec \\\n-\t\t\t[list git submodule update --init --recursive] \\\n-\t\t\t[cb _do_validate_submodule_cloning]\n-\t} else {\n-\t\tset done 1\n-\t}\n-}\n-\n method _readtree_wait {fd} {\n \tset buf [read $fd]\n-\t$o_cons update_meter $buf\n+\t$o_status_op update_meter $buf\n \tappend readtree_err $buf\n \n \tfconfigure $fd -blocking 1\n@@ -1050,6 +1060,34 @@ method _postcheckout_wait {fd_ph} {\n \tfconfigure $fd_ph -blocking 0\n }\n \n+method _do_clone_submodules {} {\n+\tif {$recursive eq {true}} {\n+\t\t$o_status_op stop\n+\t\tset o_status_op {}\n+\n+\t\tdestroy $w_body\n+\n+\t\tset o_cons [console::embed \\\n+\t\t\t$w_body \\\n+\t\t\t[mc \"Cloning submodules\"]]\n+\t\tpack $w_body -fill both -expand 1 -padx 10\n+\t\t$o_cons exec \\\n+\t\t\t[list git submodule update --init --recursive] \\\n+\t\t\t[cb _do_validate_submodule_cloning]\n+\t} else {\n+\t\tset done 1\n+\t}\n+}\n+\n+method _do_validate_submodule_cloning {ok} {\n+\tif {$ok} {\n+\t\t$o_cons done $ok\n+\t\tset done 1\n+\t} else {\n+\t\t_clone_failed $this [mc \"Cannot clone submodules.\"]\n+\t}\n+}\n+\n ######################################################################\n ##\n ## Open Existing Repository\ndiff --git a/lib/index.tcl b/lib/index.tcl\nindex 28d4d2a54e..62f4773ef4 100644\n--- a/lib/index.tcl\n+++ b/lib/index.tcl\n@@ -45,13 +45,13 @@ proc _close_updateindex {fd after} {\n \t\twm deiconify $w\n \t\ttkwait window $w\n \n-\t\t$::main_status stop\n+\t\t$::main_status stop_all\n \t\tunlock_index\n \t\trescan $after 0\n \t\treturn\n \t}\n \n-\t$::main_status stop\n+\t$::main_status stop_all\n \tunlock_index\n \tuplevel #0 $after\n }\n@@ -67,7 +67,7 @@ proc update_indexinfo {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index -z --index-info]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -81,15 +81,18 @@ proc update_indexinfo {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_indexinfo {fd path_list total_cnt batch after} {\n+proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -119,7 +122,7 @@ proc write_update_indexinfo {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc update_index {msg path_list after} {\n@@ -133,7 +136,7 @@ proc update_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write update-index --add --remove -z --stdin]\n \tfconfigure $fd \\\n \t\t-blocking 0 \\\n@@ -147,15 +150,18 @@ proc update_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_update_index {fd path_list total_cnt batch after} {\n+proc write_update_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -190,7 +196,7 @@ proc write_update_index {fd path_list total_cnt batch after} {\n \t\tdisplay_file $path $new\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc checkout_index {msg path_list after} {\n@@ -204,7 +210,7 @@ proc checkout_index {msg path_list after} {\n \tset batch [expr {int($total_cnt * .01) + 1}]\n \tif {$batch > 25} {set batch 25}\n \n-\t$::main_status start $msg [mc \"files\"]\n+\tset status_bar_operation [$::main_status start $msg [mc \"files\"]]\n \tset fd [git_write checkout-index \\\n \t\t--index \\\n \t\t--quiet \\\n@@ -224,15 +230,18 @@ proc checkout_index {msg path_list after} {\n \t\t$path_list \\\n \t\t$total_cnt \\\n \t\t$batch \\\n+\t\t$status_bar_operation \\\n \t\t$after \\\n \t\t]\n }\n \n-proc write_checkout_index {fd path_list total_cnt batch after} {\n+proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \\\n+\tafter} {\n \tglobal update_index_cp\n \tglobal file_states current_diff_path\n \n \tif {$update_index_cp >= $total_cnt} {\n+\t\t$status_bar_operation stop\n \t\t_close_updateindex $fd $after\n \t\treturn\n \t}\n@@ -253,7 +262,7 @@ proc write_checkout_index {fd path_list total_cnt batch after} {\n \t\t}\n \t}\n \n-\t$::main_status update $update_index_cp $total_cnt\n+\t$status_bar_operation update $update_index_cp $total_cnt\n }\n \n proc unstage_helper {txt paths} {\ndiff --git a/lib/merge.tcl b/lib/merge.tcl\nindex 9f253db5b3..8df8ffae55 100644\n--- a/lib/merge.tcl\n+++ b/lib/merge.tcl\n@@ -241,23 +241,27 @@ Continue with resetting the current changes?\"]\n \tif {[ask_popup $op_question] eq {yes}} {\n \t\tset fd [git_read --stderr read-tree --reset -u -v HEAD]\n \t\tfconfigure $fd -blocking 0 -translation binary\n-\t\tfileevent $fd readable [namespace code [list _reset_wait $fd]]\n-\t\t$::main_status start [mc \"Aborting\"] [mc \"files reset\"]\n+\t\tset status_bar_operation [$::main_status \\\n+\t\t\tstart \\\n+\t\t\t[mc \"Aborting\"] \\\n+\t\t\t[mc \"files reset\"]\n+\t\tfileevent $fd readable [namespace code [list \\\n+\t\t\t_reset_wait $fd $status_bar_operation]]\n \t} else {\n \t\tunlock_index\n \t}\n }\n \n-proc _reset_wait {fd} {\n+proc _reset_wait {fd status_bar_operation} {\n \tglobal ui_comm\n \n-\t$::main_status update_meter [read $fd]\n+\t$status_bar_operation update_meter [read $fd]\n \n \tfconfigure $fd -blocking 1\n \tif {[eof $fd]} {\n \t\tset fail [catch {close $fd} err]\n-\t\t$::main_status stop\n \t\tunlock_index\n+\t\t$status_bar_operation stop\n \n \t\t$ui_comm delete 0.0 end\n \t\t$ui_comm edit modified false\ndiff --git a/lib/status_bar.tcl b/lib/status_bar.tcl\nindex 02111a1742..d32b14142f 100644\n--- a/lib/status_bar.tcl\n+++ b/lib/status_bar.tcl\n@@ -1,16 +1,42 @@\n # git-gui status bar mega-widget\n # Copyright (C) 2007 Shawn Pearce\n \n+# The status_bar class manages the entire status bar. It is possible for\n+# multiple overlapping asynchronous operations to want to display status\n+# simultaneously. Each one receives a status_bar_operation when it calls the\n+# start method, and the status bar combines all active operations into the\n+# line of text it displays. Most of the time, there will be at most one\n+# ongoing operation.\n+#\n+# Note that the entire status bar can be either in single-line or two-line\n+# mode, depending on the constructor. Multiple active operations are only\n+# supported for single-line status bars.\n+\n class status_bar {\n \n+field allow_multiple ; # configured at construction\n+\n field w         ; # our own window path\n field w_l       ; # text widget we draw messages into\n field w_c       ; # canvas we draw a progress bar into\n field c_pack    ; # script to pack the canvas with\n-field status  {}; # single line of text we show\n-field prefix  {}; # text we format into status\n-field units   {}; # unit of progress\n-field meter   {}; # current core git progress meter (if active)\n+\n+field baseline_text   ; # text to show if there are no operations\n+field status_bar_text ; # combined text for all operations\n+\n+field operations ; # list of current ongoing operations\n+\n+# The status bar can display a progress bar, updated when consumers call the\n+# update method on their status_bar_operation. When there are multiple\n+# operations, the status bar shows the combined status of all operations.\n+#\n+# When an overlapping operation completes, the progress bar is going to\n+# abruptly have one fewer operation in the calculation, causing a discontinuity.\n+# Therefore, whenever an operation completes, if it is not the last operation,\n+# this counter is increased, and the progress bar is calculated as though there\n+# were still another operation at 100%. When the last operation completes, this\n+# is reset to 0.\n+field completed_operation_count\n \n constructor new {path} {\n \tglobal use_ttk NS\n@@ -18,12 +44,19 @@ constructor new {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Standard single-line status bar: Permit overlapping operations\n+\tset allow_multiple 1\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \tif {!$use_ttk} {\n \t\t$w configure -borderwidth 1 -relief sunken\n \t}\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -side left\n@@ -44,9 +77,16 @@ constructor two_line {path} {\n \tset w_l $w.l\n \tset w_c $w.c\n \n+\t# Two-line status bar: Only one ongoing operation permitted.\n+\tset allow_multiple 0\n+\n+\tset baseline_text \"\"\n+\tset operations [list]\n+\tset completed_operation_count 0\n+\n \t${NS}::frame $w\n \t${NS}::label $w_l \\\n-\t\t-textvariable @status \\\n+\t\t-textvariable @status_bar_text \\\n \t\t-anchor w \\\n \t\t-justify left\n \tpack $w_l -anchor w -fill x\n@@ -56,7 +96,7 @@ constructor two_line {path} {\n \treturn $this\n }\n \n-method start {msg uds} {\n+method ensure_canvas {} {\n \tif {[winfo exists $w_c]} {\n \t\t$w_c coords bar 0 0 0 20\n \t} else {\n@@ -68,31 +108,170 @@ method start {msg uds} {\n \t\t$w_c create rectangle 0 0 0 20 -tags bar -fill navy\n \t\teval $c_pack\n \t}\n+}\n+\n+method show {msg} {\n+\t$this ensure_canvas\n+\tset baseline_text $msg\n+\t$this refresh\n+}\n+\n+method start {msg {uds {}}} {\n+\tset baseline_text \"\"\n+\n+\tif {!$allow_multiple && [llength $operations]} {\n+\t\treturn [lindex $operations 0]\n+\t}\n+\n+\t$this ensure_canvas\n+\n+\tset operation [status_bar_operation::new $this $msg $uds]\n+\n+\tlappend operations $operation\n+\n+\t$this refresh\n+\n+\treturn $operation\n+}\n+\n+method refresh {} {\n+\tset new_text \"\"\n+\n+\tset total [expr $completed_operation_count * 100]\n+\tset have $total\n+\n+\tforeach operation $operations {\n+\t\tif {$new_text != \"\"} {\n+\t\t\tappend new_text \" / \"\n+\t\t}\n+\n+\t\tappend new_text [$operation get_status]\n+\n+\t\tset total [expr $total + 100]\n+\t\tset have [expr $have + [$operation get_progress]]\n+\t}\n+\n+\tif {$new_text == \"\"} {\n+\t\tset new_text $baseline_text\n+\t}\n+\n+\tset status_bar_text $new_text\n+\n+\tif {[winfo exists $w_c]} {\n+\t\tset pixel_width 0\n+\t\tif {$have > 0} {\n+\t\t\tset pixel_width [expr {[winfo width $w_c] * $have / $total}]\n+\t\t}\n+\n+\t\t$w_c coords bar 0 0 $pixel_width 20\n+\t}\n+}\n+\n+method stop {operation stop_msg} {\n+\tset idx [lsearch $operations $operation]\n+\n+\tif {$idx >= 0} {\n+\t\tset operations [lreplace $operations $idx $idx]\n+\t\tset completed_operation_count [expr \\\n+\t\t\t$completed_operation_count + 1]\n+\n+\t\tif {[llength $operations] == 0} {\n+\t\t\tset completed_operation_count 0\n+\n+\t\t\tdestroy $w_c\n+\t\t\tif {$stop_msg ne {}} {\n+\t\t\t\tset baseline_text $stop_msg\n+\t\t\t}\n+\t\t}\n+\n+\t\t$this refresh\n+\t}\n+}\n+\n+method stop_all {{stop_msg {}}} {\n+\t# This makes the operation's call to stop a no-op.\n+\tset operations_copy $operations\n+\tset operations [list]\n+\n+\tforeach operation $operations_copy {\n+\t\t$operation stop\n+\t}\n+\n+\tif {$stop_msg ne {}} {\n+\t\tset baseline_text $stop_msg\n+\t}\n+\n+\t$this refresh\n+}\n+\n+method _delete {current} {\n+\tif {$current eq $w} {\n+\t\tdelete_this\n+\t}\n+}\n+\n+}\n+\n+# The status_bar_operation class tracks a single consumer's ongoing status bar\n+# activity, with the context that there are a few situations where multiple\n+# overlapping asynchronous operations might want to display status information\n+# simultaneously. Instances of status_bar_operation are created by calling\n+# start on the status_bar, and when the caller is done with its stauts bar\n+# operation, it calls stop on the operation.\n+\n+class status_bar_operation {\n+\n+field status_bar; # reference back to the status_bar that owns this object\n+\n+field is_active;\n+\n+field status   {}; # single line of text we show\n+field progress {}; # current progress (0 to 100)\n+field prefix   {}; # text we format into status\n+field units    {}; # unit of progress\n+field meter    {}; # current core git progress meter (if active)\n+\n+constructor new {owner msg uds} {\n+\tset status_bar $owner\n \n \tset status $msg\n+\tset progress 0\n \tset prefix $msg\n \tset units  $uds\n \tset meter  {}\n+\n+\tset is_active 1\n+\n+\treturn $this\n }\n \n+method get_is_active {} { return $is_active }\n+method get_status {} { return $status }\n+method get_progress {} { return $progress }\n+\n method update {have total} {\n-\tset pdone 0\n-\tset cdone 0\n+\tif {!$is_active} { return }\n+\n+\tset progress 0\n+\n \tif {$total > 0} {\n-\t\tset pdone [expr {100 * $have / $total}]\n-\t\tset cdone [expr {[winfo width $w_c] * $have / $total}]\n+\t\tset progress [expr {100 * $have / $total}]\n \t}\n \n \tset prec [string length [format %i $total]]\n+\n \tset status [mc \"%s ... %*i of %*i %s (%3i%%)\" \\\n \t\t$prefix \\\n \t\t$prec $have \\\n \t\t$prec $total \\\n-\t\t$units $pdone]\n-\t$w_c coords bar 0 0 $cdone 20\n+\t\t$units $progress]\n+\n+\t$status_bar refresh\n }\n \n method update_meter {buf} {\n+\tif {!$is_active} { return }\n+\n \tappend meter $buf\n \tset r [string last \"\\r\" $meter]\n \tif {$r == -1} {\n@@ -109,23 +288,25 @@ method update_meter {buf} {\n \t}\n }\n \n-method stop {{msg {}}} {\n-\tdestroy $w_c\n-\tif {$msg ne {}} {\n-\t\tset status $msg\n+method stop {{stop_msg {}}} {\n+\tif {$is_active} {\n+\t\tset is_active 0\n+\t\t$status_bar stop $this $stop_msg\n \t}\n }\n \n-method show {msg {test {}}} {\n-\tif {$test eq {} || $status eq $test} {\n-\t\tset status $msg\n-\t}\n+method restart {msg} {\n+\tif {!$is_active} { return }\n+\n+\tset status $msg\n+\tset prefix $msg\n+\tset meter {}\n+\t$status_bar refresh\n }\n \n-method _delete {current} {\n-\tif {$current eq $w} {\n-\t\tdelete_this\n-\t}\n+method _delete {} {\n+\tstop\n+\tdelete_this\n }\n \n }\n-- \ngitgitgadget\n\n"},{"id":"387372","messageId":"90eb7e96-6352-deb9-0c22-14993ce42c8e@iee.email","threadId":"52154","inReplyTo":"20191130230543.p5xtapnx5a56arng@yadavpratyush.com","subject":"Re: [PATCH v6 2/3] git-gui: update status bar to track operations","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.email","sentAt":"2019-12-01T11:43:41Z","receivedAt":"2019-12-01T11:43:46Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"On 30/11/2019 23:05, Pratyush Yadav wrote:\n > On 28/11/19 08:30AM, Jonathan Gilbert via GitGitGadget wrote:\n>> +# Operation displayed by status mega-widget during _do_clone_checkout =>\n>> +# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n>> +# _do_validate_submodule_cloning. The status mega-widget is a difference\nshould this be \"different\", rather than 'difference'?\n\n>> +# instance than that stored in $o_status in earlier operations.\n> The last sentence doesn't make a lot of sense to me. What is \"earlier\n> operations\"? If this refers to previous versions of this file, then I\n> don't think such a comment belongs here. It should be in the commit\n> message instead.\n>\nPhilip\n"},{"id":"387382","messageId":"CAPSOpYuLWzqYBvP_4y__UTV_Krv0A2FqKKQir-xadWNbN5zq-Q@mail.gmail.com","threadId":"52154","inReplyTo":"90eb7e96-6352-deb9-0c22-14993ce42c8e@iee.email","subject":"Re: [PATCH v6 2/3] git-gui: update status bar to track operations","fromName":"Jonathan Gilbert","fromEmail":"logic@deltaq.org","sentAt":"2019-12-01T20:09:54Z","receivedAt":"2019-12-01T20:10:21Z","isPatch":true,"sender":{"key":"logic@deltaq.org","avatar":"https://gravatar.com/avatar/6aa0c0a07ad7988ae46c3c5319d4bbdaa73773b49fb7e668c4bb0aad34b8f67f?d=mp&s=160"},"body":"On Sun, Dec 1, 2019 at 5:43 AM Philip Oakley philipoakley-at-iee.email\n|GitHub Public/Example Allow| <ogvdf9gsg7oxult@sneakemail.com> wrote:\n> On 30/11/2019 23:05, Pratyush Yadav wrote:\n>  > On 28/11/19 08:30AM, Jonathan Gilbert via GitGitGadget wrote:\n> >> +# Operation displayed by status mega-widget during _do_clone_checkout =>\n> >> +# _readtree_wait => _postcheckout_wait => _do_clone_submodules =>\n> >> +# _do_validate_submodule_cloning. The status mega-widget is a difference\n>\n> should this be \"different\", rather than 'difference'?\n\nIt absolutely should and I have corrected that in the re-roll.\n\nThanks :-)\n\nJonathan\n"},{"id":"387574","messageId":"20191205185420.laivdwmmwuukptxu@yadavpratyush.com","threadId":"52154","inReplyTo":"pull.436.v7.git.1575167313.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 0/3] git-gui: revert untracked files by deleting them","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2019-12-05T18:54:20Z","receivedAt":"2019-12-05T18:54:34Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Jonathan,\n\nOn 01/12/19 02:28AM, Jonathan Gilbert via GitGitGadget wrote:\n> My development environment sometimes makes automatic changes that I don't\n> want to keep. In some cases, this involves new files being added that I\n> don't want to commit or keep (but I also don't want to outright .gitignore \n> forever). I have typically had to explicitly delete those files externally\n> to Git Gui, which is a context switch to a manual operation, and I want to\n> be able to just select those newly-created untracked files in the UI and\n> \"revert\" them into oblivion.\n> \n> This change updates the revert_helper proc to check for untracked files as\n> well as changes, and then changes to be reverted and untracked files are\n> handled by independent blocks of code. The user is prompted independently\n> for untracked files, since the underlying action is fundamentally different\n> (rm -f). If after deleting untracked files, the directory containing them\n> becomes empty, then the directory is removed as well. A new proc \n> delete_files takes care of actually deleting the files, using the Tcler's\n> Wiki recommended approach for keeping the UI responsive.\n> \n> Since the checkout_index and delete_files calls are both asynchronous and\n> could potentially complete in any order, a \"chord\" is used to coordinate\n> unlocking the index and returning the UI to a usable state only after both\n> operations are complete.\n> \n> Since the checkout_index and delete_files calls are both asynchronous and\n> overlap, they clash in wanting to update the status bar. To address this,\n> the status bar is reworked so that when an operation wants to display\n> ongoing updates/progress, it explicitly starts an \"operation\", which is\n> tracked by its own object, and the status bar handles multiple concurrent\n> operations by merging their progress and concatenating their text. This is\n> captured in a separate commit, since it touches a variety of files.\n> \n> The _close_updateindex proc contains error handling (added in d4e890e5) that\n> has the potential to interact badly with unlock_index running at the\n> completion of an async operation. I have refactored the procedure into\n> separate procs _close_updateindex and rescan_on_error. Call sites that\n> exercised the combined functionality also unlocked the index, so a combined\n> proc close_and_unlock_index calls _close_updateindex and then either \n> rescan_on_error or unlock_index as appropriate. Call sites have been updated\n> appropriately.\n> \n> The revert_helper proc, with its overlapping operations, is an example of a\n> call site that does not combine the close and unlock/rescan operations. The \n> checkout_index proc has been reworked to only call _close_updateindex, and\n> to call a functor supplied by the caller to captures any errors that occur. \n> revert_helper uses this to supply a lambda function that stashes the error\n> within the chord's body namespace, so that it can then separately call \n> rescan_on_error when the chord is completed (or unlock_index, if no error\n> was captured), which might be substantially after checkout_index encounters\n> its error. If it turns out that a rescan is called for, it is done once the\n> deletion is complete.\n> \n> This is the eighth revision of this change, which differs from the seventh\n> version in the following ways (most of which are in the second of the three\n> commits, to do with the status bar rework):\n> \n>  * The bump of the Tcl/Tk dependency from 8.4 to 8.6 now takes place in the\n>    third commit, where it is needed and whose commit message actually calls\n>    it out.\n>    \n>    \n>  * The show method in status_bar_operation has been renamed to restart, and\n>    the meter is cleared. Also, the supplied message is set as the prefix for\n>    future update calls.\n>    \n>    \n>  * The call site for $status_operation show in blame.tcl has been\n>    corresponding changed to $status_operation restart.\n>    \n>    \n>  * A typo has been corrected in a comment. :-)\n\nThanks for the quality contribution. Merged to 'master'. Will push it \nout soon.\n\n-- \nRegards,\nPratyush Yadav\n"},{"id":"392513","messageId":"20200226082409.GA2270319@f3","threadId":"52154","inReplyTo":"8fe9dfc30771574344c1a2fc5cd8f21f1a617388.1575167313.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v7 2/3] git-gui: update status bar to track operations","fromName":"Benjamin Poirier","fromEmail":"benjamin.poirier@gmail.com","sentAt":"2020-02-26T08:24:09Z","receivedAt":"2020-02-26T08:24:18Z","isPatch":true,"sender":{"key":"benjamin.poirier@gmail.com","avatar":null},"body":"On 2019/12/01 02:28 +0000, Jonathan Gilbert via GitGitGadget wrote:\n> From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> \n> Update the status bar to track updates as individual \"operations\" that\n> can overlap. Update all call sites to interact with the new status bar\n> mechanism. Update initialization to explicitly clear status text,\n> since otherwise it may persist across future operations.\n> \n> Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> ---\n\nHi Jonathan,\n\nIt appears that this change has caused a regression when using git-gui\nblame <file> -> right click on a source line -> \"Show History Context\"\n\nThere is an \"Application Error\" window that appears with the following\ndetails:\ncan't read \"::main_status\": no such variable\ncan't read \"::main_status\": no such variable\n    while executing\n\"$::main_status  start  [mc \"Starting %s... please wait...\" \"gitk\"]\"\n    (procedure \"do_gitk\" line 55)\n    invoked from within\n\"do_gitk $cmdline\"\n    (procedure \"blame::_gitkcommit\" line 47)\n    invoked from within\n\"blame::_gitkcommit ::blame::__o1::__d\"\n    invoked from within\n\".ctxm invoke active\"\n    (\"uplevel\" body line 1)\n    invoked from within\n\"uplevel #0 [list $w invoke active]\"\n    (procedure \"tk::MenuInvoke\" line 50)\n    invoked from within\n\"tk::MenuInvoke .ctxm 1\"\n    (command bound to event)\n\nThe rest of the functionality seems unaffected but it's pretty annoying\nto have to dismiss this message each time.\n\nCan you please look into it?\n"},{"id":"392745","messageId":"20200302181452.gssh43l2sxu7kqak@yadavpratyush.com","threadId":"52154","inReplyTo":"20200226082409.GA2270319@f3","subject":"Re: [PATCH v7 2/3] git-gui: update status bar to track operations","fromName":"Pratyush Yadav","fromEmail":"me@yadavpratyush.com","sentAt":"2020-03-02T18:14:52Z","receivedAt":"2020-03-02T18:15:01Z","isPatch":true,"sender":{"key":"me@yadavpratyush.com","avatar":"https://avatars.githubusercontent.com/u/8817931?v=4"},"body":"Hi Benjamin,\n\nOn 26/02/20 05:24PM, Benjamin Poirier wrote:\n> On 2019/12/01 02:28 +0000, Jonathan Gilbert via GitGitGadget wrote:\n> > From: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> > \n> > Update the status bar to track updates as individual \"operations\" that\n> > can overlap. Update all call sites to interact with the new status bar\n> > mechanism. Update initialization to explicitly clear status text,\n> > since otherwise it may persist across future operations.\n> > \n> > Signed-off-by: Jonathan Gilbert <JonathanG@iQmetrix.com>\n> > ---\n> \n> Hi Jonathan,\n> \n> It appears that this change has caused a regression when using git-gui\n> blame <file> -> right click on a source line -> \"Show History Context\"\n> \n> There is an \"Application Error\" window that appears with the following\n> details:\n> can't read \"::main_status\": no such variable\n> can't read \"::main_status\": no such variable\n>     while executing\n> \"$::main_status  start  [mc \"Starting %s... please wait...\" \"gitk\"]\"\n>     (procedure \"do_gitk\" line 55)\n>     invoked from within\n> \"do_gitk $cmdline\"\n>     (procedure \"blame::_gitkcommit\" line 47)\n>     invoked from within\n> \"blame::_gitkcommit ::blame::__o1::__d\"\n>     invoked from within\n> \".ctxm invoke active\"\n>     (\"uplevel\" body line 1)\n>     invoked from within\n> \"uplevel #0 [list $w invoke active]\"\n>     (procedure \"tk::MenuInvoke\" line 50)\n>     invoked from within\n> \"tk::MenuInvoke .ctxm 1\"\n>     (command bound to event)\n> \n> The rest of the functionality seems unaffected but it's pretty annoying\n> to have to dismiss this message each time.\n> \n> Can you please look into it?\n\nDoes the following patch fix the problem? I will submit a proper patch \nsoon.\n\n-----8<-----\ndiff --git a/git-gui.sh b/git-gui.sh\nindex f41ed2e..d939844 100755\n--- a/git-gui.sh\n+++ b/git-gui.sh\n@@ -2205,11 +2205,13 @@ proc do_gitk {revs {is_submodule false}} {\n \t\tset env(GIT_WORK_TREE) $_gitworktree\n \t\tcd $pwd\n\n-\t\tset status_operation [$::main_status \\\n-\t\t\tstart \\\n-\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n+\t\tif {[info exists main_status]} {\n+\t\t\tset status_operation [$::main_status \\\n+\t\t\t\tstart \\\n+\t\t\t\t[mc \"Starting %s... please wait...\" \"gitk\"]]\n\n-\t\tafter 3500 [list $status_operation stop]\n+\t\t\tafter 3500 [list $status_operation stop]\n+\t\t}\n \t}\n }\n-----8<-----\n\n-- \nRegards,\nPratyush Yadav\n"}]}