# git-gui index (add/remove) support # Copyright (C) 2006, 2007 Shawn Pearce proc _delete_indexlock {} { if {[catch {file delete -- [gitdir index.lock]} err]} { error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"] } } proc close_and_unlock_index {fd after} { if {![catch {_close_updateindex $fd} err]} { unlock_index uplevel #0 $after } else { rescan_on_error $err $after } } proc _close_updateindex {fd} { fconfigure $fd -blocking 1 close $fd } proc rescan_on_error {err {after {}}} { global use_ttk NS set w .indexfried Dialog $w wm withdraw $w wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]] wm geometry $w "+[winfo rootx .]+[winfo rooty .]" set s [mc "Updating the Git index failed. A rescan will be automatically started to resynchronize git-gui."] text $w.msg -yscrollcommand [list $w.vs set] \ -width [string length $s] -relief flat \ -borderwidth 0 -highlightthickness 0 \ -background [get_bg_color $w] $w.msg tag configure bold -font font_uibold -justify center ${NS}::scrollbar $w.vs -command [list $w.msg yview] $w.msg insert end $s bold \n\n$err {} $w.msg configure -state disabled ${NS}::button $w.continue \ -text [mc "Continue"] \ -command [list destroy $w] ${NS}::button $w.unlock \ -text [mc "Unlock Index"] \ -command "destroy $w; _delete_indexlock" grid $w.msg - $w.vs -sticky news grid $w.unlock $w.continue - -sticky se -padx 2 -pady 2 grid columnconfigure $w 0 -weight 1 grid rowconfigure $w 0 -weight 1 wm protocol $w WM_DELETE_WINDOW update bind $w.continue " grab $w focus %W " wm deiconify $w tkwait window $w $::main_status stop_all unlock_index rescan [concat $after {ui_ready;}] 0 } proc update_indexinfo {msg path_list after} { global update_index_cp if {![lock_index update]} return set update_index_cp 0 set path_list [lsort $path_list] set total_cnt [llength $path_list] set batch [expr {int($total_cnt * .01) + 1}] if {$batch > 25} {set batch 25} set status_bar_operation [$::main_status start $msg [mc "files"]] set fd [git_write update-index -z --index-info] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_indexinfo \ $fd \ $path_list \ $total_cnt \ $batch \ $status_bar_operation \ $after \ ] } proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \ after} { global update_index_cp global file_states current_diff_path if {$update_index_cp >= $total_cnt} { $status_bar_operation stop close_and_unlock_index $fd $after return } for {set i $batch} \ {$update_index_cp < $total_cnt && $i > 0} \ {incr i -1} { set path [lindex $path_list $update_index_cp] incr update_index_cp set s $file_states($path) switch -glob -- [lindex $s 0] { A? {set new _O} MT - TM - T_ {set new _T} M? {set new _M} TD - D_ {set new _D} D? {set new _?} ?? {continue} } set info [lindex $s 2] if {$info eq {}} continue puts -nonewline $fd "$info\t[encoding convertto utf-8 $path]\0" display_file $path $new } $status_bar_operation update $update_index_cp $total_cnt } proc update_index {msg path_list after} { global update_index_cp if {![lock_index update]} return set update_index_cp 0 set path_list [lsort $path_list] set total_cnt [llength $path_list] set batch [expr {int($total_cnt * .01) + 1}] if {$batch > 25} {set batch 25} set status_bar_operation [$::main_status start $msg [mc "files"]] set fd [git_write update-index --add --remove -z --stdin] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_update_index \ $fd \ $path_list \ $total_cnt \ $batch \ $status_bar_operation \ $after \ ] } proc write_update_index {fd path_list total_cnt batch status_bar_operation \ after} { global update_index_cp global file_states current_diff_path if {$update_index_cp >= $total_cnt} { $status_bar_operation stop close_and_unlock_index $fd $after return } for {set i $batch} \ {$update_index_cp < $total_cnt && $i > 0} \ {incr i -1} { set path [lindex $path_list $update_index_cp] incr update_index_cp switch -glob -- [lindex $file_states($path) 0] { AD {set new __} ?D {set new D_} _O - AT - AM {set new A_} TM - MT - _T {set new T_} _U - U? { if {[file exists $path]} { set new M_ } else { set new D_ } } ?M {set new M_} ?? {continue} } puts -nonewline $fd "[encoding convertto utf-8 $path]\0" display_file $path $new } $status_bar_operation update $update_index_cp $total_cnt } proc checkout_index {msg path_list after capture_error} { global update_index_cp if {![lock_index update]} return set update_index_cp 0 set path_list [lsort $path_list] set total_cnt [llength $path_list] set batch [expr {int($total_cnt * .01) + 1}] if {$batch > 25} {set batch 25} set status_bar_operation [$::main_status start $msg [mc "files"]] set fd [git_write checkout-index \ --index \ --quiet \ --force \ -z \ --stdin \ ] fconfigure $fd \ -blocking 0 \ -buffering full \ -buffersize 512 \ -encoding binary \ -translation binary fileevent $fd writable [list \ write_checkout_index \ $fd \ $path_list \ $total_cnt \ $batch \ $status_bar_operation \ $after \ $capture_error \ ] } proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \ after capture_error} { global update_index_cp global file_states current_diff_path if {$update_index_cp >= $total_cnt} { $status_bar_operation stop # We do not unlock the index directly here because this # operation expects to potentially run in parallel with file # deletions scheduled by revert_helper. We're done with the # update index, so we close it, but actually unlocking the index # and dealing with potential errors is deferred to the chord # body that runs when all async operations are completed. # # (See after_chord in revert_helper.) if {[catch {_close_updateindex $fd} err]} { uplevel #0 $capture_error [list $err] } uplevel #0 $after return } for {set i $batch} \ {$update_index_cp < $total_cnt && $i > 0} \ {incr i -1} { set path [lindex $path_list $update_index_cp] incr update_index_cp switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - ?T - ?D { puts -nonewline $fd "[encoding convertto utf-8 $path]\0" display_file $path ?_ } } } $status_bar_operation update $update_index_cp $total_cnt } proc unstage_helper {txt paths} { global file_states current_diff_path if {![lock_index begin-update]} return set path_list [list] set after {} foreach path $paths { switch -glob -- [lindex $file_states($path) 0] { A? - M? - T? - D? { lappend path_list $path if {$path eq $current_diff_path} { set after {reshow_diff;} } } } } if {$path_list eq {}} { unlock_index } else { update_indexinfo \ $txt \ $path_list \ [concat $after {ui_ready;}] } } proc do_unstage_selection {} { global current_diff_path selected_paths if {[array size selected_paths] > 0} { unstage_helper \ [mc "Unstaging selected files from commit"] \ [array names selected_paths] } elseif {$current_diff_path ne {}} { unstage_helper \ [mc "Unstaging %s from commit" [short_path $current_diff_path]] \ [list $current_diff_path] } } proc add_helper {txt paths} { global file_states current_diff_path if {![lock_index begin-update]} return set path_list [list] set after {} foreach path $paths { switch -glob -- [lindex $file_states($path) 0] { _U - U? { if {$path eq $current_diff_path} { unlock_index merge_stage_workdir $path return } } _O - ?M - ?D - ?T { lappend path_list $path if {$path eq $current_diff_path} { set after {reshow_diff;} } } } } if {$path_list eq {}} { unlock_index } else { update_index \ $txt \ $path_list \ [concat $after {ui_status [mc "Ready to commit."];}] } } proc do_add_selection {} { global current_diff_path selected_paths if {[array size selected_paths] > 0} { add_helper \ [mc "Adding selected files"] \ [array names selected_paths] } elseif {$current_diff_path ne {}} { add_helper \ [mc "Adding %s" [short_path $current_diff_path]] \ [list $current_diff_path] } } proc do_add_all {} { global file_states set paths [list] set untracked_paths [list] foreach path [array names file_states] { switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - ?T - ?D {lappend paths $path} ?O {lappend untracked_paths $path} } } if {[llength $untracked_paths]} { set reply 0 switch -- [get_config gui.stageuntracked] { no { set reply 0 } yes { set reply 1 } ask - default { set reply [ask_popup [mc "Stage %d untracked files?" \ [llength $untracked_paths]]] } } if {$reply} { set paths [concat $paths $untracked_paths] } } add_helper [mc "Adding all changed files"] $paths } # Copied from TclLib package "lambda". proc lambda {arguments body args} { return [list ::apply [list $arguments $body] {*}$args] } proc revert_helper {txt paths} { global file_states current_diff_path if {![lock_index begin-update]} return # Common "after" functionality that waits until multiple asynchronous # operations are complete (by waiting for them to activate their notes # on the chord). # # The asynchronous operations are each indicated below by a comment # before the code block that starts the async operation. set after_chord [SimpleChord::new { if {[string trim $err] != ""} { rescan_on_error $err } else { unlock_index if {$should_reshow_diff} { reshow_diff } ui_ready } }] $after_chord eval { set should_reshow_diff 0 } # This function captures an error for processing when after_chord is # completed. (The chord is curried into the lambda function.) set capture_error [lambda \ {chord error} \ { $chord eval [list set err $error] } \ $after_chord] # We don't know how many notes we're going to create (it's dynamic based # on conditional paths below), so create a common note that will delay # the chord's completion until we activate it, and then activate it # after all the other notes have been created. set after_common_note [$after_chord add_note] set path_list [list] set untracked_list [list] foreach path $paths { switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?O { lappend untracked_list $path } ?M - ?T - ?D { lappend path_list $path if {$path eq $current_diff_path} { $after_chord eval { set should_reshow_diff 1 } } } } } set path_cnt [llength $path_list] set untracked_cnt [llength $untracked_list] # Asynchronous operation: revert changes by checking them out afresh # from the index. if {$path_cnt > 0} { # Split question between singular and plural cases, because # such distinction is needed in some languages. Previously, the # code used "Revert changes in" for both, but that can't work # in languages where 'in' must be combined with word from # rest of string (in different way for both cases of course). # # FIXME: Unfortunately, even that isn't enough in some languages # as they have quite complex plural-form rules. Unfortunately, # msgcat doesn't seem to support that kind of string # translation. # if {$path_cnt == 1} { set query [mc \ "Revert changes in file %s?" \ [short_path [lindex $path_list]] \ ] } else { set query [mc \ "Revert changes in these %i files?" \ $path_cnt] } set reply [tk_dialog \ .confirm_revert \ "[appname] ([reponame])" \ "$query [mc "Any unstaged changes will be permanently lost by the revert."]" \ question \ 1 \ [mc "Do Nothing"] \ [mc "Revert Changes"] \ ] if {$reply == 1} { set note [$after_chord add_note] checkout_index \ $txt \ $path_list \ [list $note activate] \ $capture_error } } # Asynchronous operation: Deletion of untracked files. if {$untracked_cnt > 0} { # Split question between singular and plural cases, because # such distinction is needed in some languages. # # FIXME: Unfortunately, even that isn't enough in some languages # as they have quite complex plural-form rules. Unfortunately, # msgcat doesn't seem to support that kind of string # translation. # if {$untracked_cnt == 1} { set query [mc \ "Delete untracked file %s?" \ [short_path [lindex $untracked_list]] \ ] } else { set query [mc \ "Delete these %i untracked files?" \ $untracked_cnt \ ] } set reply [tk_dialog \ .confirm_revert \ "[appname] ([reponame])" \ "$query [mc "Files will be permanently deleted."]" \ question \ 1 \ [mc "Do Nothing"] \ [mc "Delete Files"] \ ] if {$reply == 1} { $after_chord eval { set should_reshow_diff 1 } set note [$after_chord add_note] delete_files $untracked_list [list $note activate] } } # Activate the common note. If no other notes were created, this # completes the chord. If other notes were created, then this common # note prevents a race condition where the chord might complete early. $after_common_note activate } # Delete all of the specified files, performing deletion in batches to allow the # UI to remain responsive and updated. proc delete_files {path_list after} { # Enable progress bar status updates set status_bar_operation [$::main_status \ start \ [mc "Deleting"] \ [mc "files"]] set path_index 0 set deletion_errors [list] set batch_size 50 delete_helper \ $path_list \ $path_index \ $deletion_errors \ $batch_size \ $status_bar_operation \ $after } # Helper function to delete a list of files in batches. Each call deletes one # batch of files, and then schedules a call for the next batch after any UI # messages have been processed. proc delete_helper {path_list path_index deletion_errors batch_size \ status_bar_operation after} { global file_states set path_cnt [llength $path_list] set batch_remaining $batch_size while {$batch_remaining > 0} { if {$path_index >= $path_cnt} { break } set path [lindex $path_list $path_index] set deletion_failed [catch {file delete -- $path} deletion_error] if {$deletion_failed} { lappend deletion_errors [list "$deletion_error"] } else { remove_empty_directories [file dirname $path] # Don't assume the deletion worked. Remove the file from # the UI, but only if it no longer exists. if {![path_exists $path]} { unset file_states($path) display_file $path __ } } incr path_index 1 incr batch_remaining -1 } # Update the progress bar to indicate that this batch has been # completed. The update will be visible when this procedure returns # and allows the UI thread to process messages. $status_bar_operation update $path_index $path_cnt if {$path_index < $path_cnt} { # The Tcler's Wiki lists this as the best practice for keeping # a UI active and processing messages during a long-running # operation. after idle [list after 0 [list \ delete_helper \ $path_list \ $path_index \ $deletion_errors \ $batch_size \ $status_bar_operation \ $after ]] } else { # Finish the status bar operation. $status_bar_operation stop # Report error, if any, based on how many deletions failed. set deletion_error_cnt [llength $deletion_errors] if {($deletion_error_cnt > 0) && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} { set error_text [mc "Encountered errors deleting files:\n"] foreach deletion_error $deletion_errors { append error_text "* [lindex $deletion_error 0]\n" } error_popup $error_text } elseif {$deletion_error_cnt == $path_cnt} { error_popup [mc \ "None of the %d selected files could be deleted." \ $path_cnt \ ] } elseif {$deletion_error_cnt > 1} { error_popup [mc \ "%d of the %d selected files could not be deleted." \ $deletion_error_cnt \ $path_cnt \ ] } uplevel #0 $after } } proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; } # This function is from the TCL documentation: # # https://wiki.tcl-lang.org/page/file+exists # # [file exists] returns false if the path does exist but is a symlink to a path # that doesn't exist. This proc returns true if the path exists, regardless of # whether it is a symlink and whether it is broken. proc path_exists {name} { expr {![catch {file lstat $name finfo}]} } # Remove as many empty directories as we can starting at the specified path, # walking up the directory tree. If we encounter a directory that is not # empty, or if a directory deletion fails, then we stop the operation and # return to the caller. Even if this procedure fails to delete any # directories at all, it does not report failure. proc remove_empty_directories {directory_path} { set parent_path [file dirname $directory_path] while {$parent_path != $directory_path} { set contents [glob -nocomplain -dir $directory_path *] if {[llength $contents] > 0} { break } if {[catch {file delete -- $directory_path}]} { break } set directory_path $parent_path set parent_path [file dirname $directory_path] } } proc do_revert_selection {} { global current_diff_path selected_paths if {[array size selected_paths] > 0} { revert_helper \ [mc "Reverting selected files"] \ [array names selected_paths] } elseif {$current_diff_path ne {}} { revert_helper \ [mc "Reverting %s" [short_path $current_diff_path]] \ [list $current_diff_path] } } proc do_select_commit_type {} { global commit_type commit_type_is_amend if {$commit_type_is_amend == 0 && [string match amend* $commit_type]} { create_new_commit } elseif {$commit_type_is_amend == 1 && ![string match amend* $commit_type]} { load_last_commit # The amend request was rejected... # if {![string match amend* $commit_type]} { set commit_type_is_amend 0 } } }