1 # git-gui index (add/remove) support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc _delete_indexlock
{} {
5 if {[catch {file delete
-- [gitdir index.lock
]} err
]} {
6 error_popup
[strcat
[mc
"Unable to unlock the index."] "\n\n$err"]
10 proc close_and_unlock_index
{fd
after} {
11 if {![catch {_close_updateindex
$fd} err
]} {
15 rescan_on_error
$err $after
19 proc _close_updateindex
{fd
} {
20 fconfigure $fd -blocking 1
24 proc rescan_on_error
{err
{after {}}} {
30 wm title
$w [strcat
"[appname] ([reponame]): " [mc
"Index Error"]]
31 wm geometry
$w "+[winfo rootx .]+[winfo rooty .]"
32 set s
[mc
"Updating the Git index failed. A rescan will be automatically started to resynchronize git-gui."]
33 text $w.msg
-yscrollcommand [list $w.vs
set] \
34 -width [string length
$s] -relief flat
\
35 -borderwidth 0 -highlightthickness 0 \
36 -background [get_bg_color
$w]
37 $w.msg tag configure bold
-font font_uibold
-justify center
38 ${NS
}::scrollbar $w.vs
-command [list $w.msg yview
]
39 $w.msg insert end
$s bold
\n\n$err {}
40 $w.msg configure
-state disabled
42 ${NS
}::button $w.
continue \
43 -text [mc
"Continue"] \
44 -command [list destroy $w]
45 ${NS
}::button $w.unlock
\
46 -text [mc
"Unlock Index"] \
47 -command "destroy $w; _delete_indexlock"
48 grid $w.msg
- $w.vs
-sticky news
49 grid $w.unlock
$w.
continue - -sticky se
-padx 2 -pady 2
50 grid columnconfigure
$w 0 -weight 1
51 grid rowconfigure
$w 0 -weight 1
53 wm protocol
$w WM_DELETE_WINDOW
update
54 bind $w.
continue <Visibility
> "
61 $::main_status stop_all
63 rescan
[concat $after {ui_ready
;}] 0
66 proc update_indexinfo
{msg path_list
after} {
67 global update_index_cp
69 if {![lock_index
update]} return
72 set path_list
[lsort $path_list]
73 set total_cnt
[llength $path_list]
74 set batch
[expr {int
($total_cnt * .01) + 1}]
75 if {$batch > 25} {set batch
25}
77 set status_bar_operation
[$::main_status start
$msg [mc
"files"]]
78 set fd
[git_write update-index
-z --index-info
]
85 fileevent $fd writable
[list \
86 write_update_indexinfo
\
91 $status_bar_operation \
96 proc write_update_indexinfo
{fd path_list total_cnt batch status_bar_operation
\
98 global update_index_cp
99 global file_states current_diff_path
101 if {$update_index_cp >= $total_cnt} {
102 $status_bar_operation stop
103 close_and_unlock_index
$fd $after
108 {$update_index_cp < $total_cnt && $i > 0} \
110 set path
[lindex $path_list $update_index_cp]
113 set s
$file_states($path)
114 switch -glob -- [lindex $s 0] {
125 set info [lindex $s 2]
126 if {$info eq
{}} continue
128 puts -nonewline $fd "$info\t[encoding convertto utf-8 $path]\0"
129 display_file
$path $new
132 $status_bar_operation update $update_index_cp $total_cnt
135 proc update_index
{msg path_list
after} {
136 global update_index_cp
138 if {![lock_index
update]} return
140 set update_index_cp
0
141 set path_list
[lsort $path_list]
142 set total_cnt
[llength $path_list]
143 set batch
[expr {int
($total_cnt * .01) + 1}]
144 if {$batch > 25} {set batch
25}
146 set status_bar_operation
[$::main_status start
$msg [mc
"files"]]
147 set fd
[git_write update-index
--add --remove -z --stdin]
154 fileevent $fd writable
[list \
160 $status_bar_operation \
165 proc write_update_index
{fd path_list total_cnt batch status_bar_operation
\
167 global update_index_cp
168 global file_states current_diff_path
170 if {$update_index_cp >= $total_cnt} {
171 $status_bar_operation stop
172 close_and_unlock_index
$fd $after
177 {$update_index_cp < $total_cnt && $i > 0} \
179 set path
[lindex $path_list $update_index_cp]
182 switch -glob -- [lindex $file_states($path) 0] {
193 if {[file exists
$path]} {
202 puts -nonewline $fd "[encoding convertto utf-8 $path]\0"
203 display_file
$path $new
206 $status_bar_operation update $update_index_cp $total_cnt
209 proc checkout_index
{msg path_list
after capture_error
} {
210 global update_index_cp
212 if {![lock_index
update]} return
214 set update_index_cp
0
215 set path_list
[lsort $path_list]
216 set total_cnt
[llength $path_list]
217 set batch
[expr {int
($total_cnt * .01) + 1}]
218 if {$batch > 25} {set batch
25}
220 set status_bar_operation
[$::main_status start
$msg [mc
"files"]]
221 set fd
[git_write checkout-index
\
234 fileevent $fd writable
[list \
235 write_checkout_index
\
240 $status_bar_operation \
246 proc write_checkout_index
{fd path_list total_cnt batch status_bar_operation
\
247 after capture_error
} {
248 global update_index_cp
249 global file_states current_diff_path
251 if {$update_index_cp >= $total_cnt} {
252 $status_bar_operation stop
254 # We do not unlock the index directly here because this
255 # operation expects to potentially run in parallel with file
256 # deletions scheduled by revert_helper. We're done with the
257 # update index, so we close it, but actually unlocking the index
258 # and dealing with potential errors is deferred to the chord
259 # body that runs when all async operations are completed.
261 # (See after_chord in revert_helper.)
263 if {[catch {_close_updateindex
$fd} err
]} {
264 uplevel #0 $capture_error [list $err]
273 {$update_index_cp < $total_cnt && $i > 0} \
275 set path
[lindex $path_list $update_index_cp]
277 switch -glob -- [lindex $file_states($path) 0] {
282 puts -nonewline $fd "[encoding convertto utf-8 $path]\0"
283 display_file
$path ?_
288 $status_bar_operation update $update_index_cp $total_cnt
291 proc unstage_helper
{txt paths
} {
292 global file_states current_diff_path
294 if {![lock_index begin-update
]} return
298 foreach path
$paths {
299 switch -glob -- [lindex $file_states($path) 0] {
304 lappend path_list
$path
305 if {$path eq
$current_diff_path} {
306 set after {reshow_diff
;}
311 if {$path_list eq
{}} {
317 [concat $after {ui_ready
;}]
321 proc do_unstage_selection
{} {
322 global current_diff_path selected_paths
324 if {[array size selected_paths
] > 0} {
326 [mc
"Unstaging selected files from commit"] \
327 [array names selected_paths
]
328 } elseif
{$current_diff_path ne
{}} {
330 [mc
"Unstaging %s from commit" [short_path
$current_diff_path]] \
331 [list $current_diff_path]
335 proc add_helper
{txt paths
} {
336 global file_states current_diff_path
338 if {![lock_index begin-update
]} return
342 foreach path
$paths {
343 switch -glob -- [lindex $file_states($path) 0] {
346 if {$path eq
$current_diff_path} {
348 merge_stage_workdir
$path
356 lappend path_list
$path
357 if {$path eq
$current_diff_path} {
358 set after {reshow_diff
;}
363 if {$path_list eq
{}} {
369 [concat $after {ui_status
[mc
"Ready to commit."];}]
373 proc do_add_selection
{} {
374 global current_diff_path selected_paths
376 if {[array size selected_paths
] > 0} {
378 [mc
"Adding selected files"] \
379 [array names selected_paths
]
380 } elseif
{$current_diff_path ne
{}} {
382 [mc
"Adding %s" [short_path
$current_diff_path]] \
383 [list $current_diff_path]
391 set untracked_paths
[list]
392 foreach path
[array names file_states
] {
393 switch -glob -- [lindex $file_states($path) 0] {
397 ?D
{lappend paths
$path}
398 ?O
{lappend untracked_paths
$path}
401 if {[llength $untracked_paths]} {
403 switch -- [get_config gui.stageuntracked
] {
412 set reply
[ask_popup
[mc
"Stage %d untracked files?" \
413 [llength $untracked_paths]]]
417 set paths
[concat $paths $untracked_paths]
420 add_helper
[mc
"Adding all changed files"] $paths
423 # Copied from TclLib package "lambda".
424 proc lambda
{arguments body args
} {
425 return [list ::apply [list $arguments $body] {*}$args]
428 proc revert_helper
{txt paths
} {
429 global file_states current_diff_path
431 if {![lock_index begin-update
]} return
433 # Common "after" functionality that waits until multiple asynchronous
434 # operations are complete (by waiting for them to activate their notes
437 # The asynchronous operations are each indicated below by a comment
438 # before the code block that starts the async operation.
439 set after_chord
[SimpleChord
::new {
440 if {[string trim
$err] != ""} {
444 if {$should_reshow_diff} { reshow_diff
}
449 $after_chord eval { set should_reshow_diff
0 }
451 # This function captures an error for processing when after_chord is
452 # completed. (The chord is curried into the lambda function.)
453 set capture_error
[lambda
\
455 { $chord eval [list set err
$error] } \
458 # We don't know how many notes we're going to create (it's dynamic based
459 # on conditional paths below), so create a common note that will delay
460 # the chord's completion until we activate it, and then activate it
461 # after all the other notes have been created.
462 set after_common_note
[$after_chord add_note
]
465 set untracked_list
[list]
467 foreach path
$paths {
468 switch -glob -- [lindex $file_states($path) 0] {
471 lappend untracked_list
$path
476 lappend path_list
$path
477 if {$path eq
$current_diff_path} {
478 $after_chord eval { set should_reshow_diff
1 }
484 set path_cnt
[llength $path_list]
485 set untracked_cnt
[llength $untracked_list]
487 # Asynchronous operation: revert changes by checking them out afresh
490 # Split question between singular and plural cases, because
491 # such distinction is needed in some languages. Previously, the
492 # code used "Revert changes in" for both, but that can't work
493 # in languages where 'in' must be combined with word from
494 # rest of string (in different way for both cases of course).
496 # FIXME: Unfortunately, even that isn't enough in some languages
497 # as they have quite complex plural-form rules. Unfortunately,
498 # msgcat doesn't seem to support that kind of string
501 if {$path_cnt == 1} {
503 "Revert changes in file %s?" \
504 [short_path
[lindex $path_list]] \
508 "Revert changes in these %i files?" \
512 set reply
[tk_dialog \
514 "[appname] ([reponame])" \
517 [mc "Any unstaged changes will be permanently lost by the revert.
"]" \
521 [mc
"Revert Changes"] \
525 set note
[$after_chord add_note
]
529 [list $note activate
] \
534 # Asynchronous operation: Deletion of untracked files.
535 if {$untracked_cnt > 0} {
536 # Split question between singular and plural cases, because
537 # such distinction is needed in some languages.
539 # FIXME: Unfortunately, even that isn't enough in some languages
540 # as they have quite complex plural-form rules. Unfortunately,
541 # msgcat doesn't seem to support that kind of string
544 if {$untracked_cnt == 1} {
546 "Delete untracked file %s?" \
547 [short_path
[lindex $untracked_list]] \
551 "Delete these %i untracked files?" \
556 set reply
[tk_dialog \
558 "[appname] ([reponame])" \
561 [mc "Files will be permanently deleted.
"]" \
565 [mc
"Delete Files"] \
569 $after_chord eval { set should_reshow_diff
1 }
571 set note
[$after_chord add_note
]
572 delete_files
$untracked_list [list $note activate
]
576 # Activate the common note. If no other notes were created, this
577 # completes the chord. If other notes were created, then this common
578 # note prevents a race condition where the chord might complete early.
579 $after_common_note activate
582 # Delete all of the specified files, performing deletion in batches to allow the
583 # UI to remain responsive and updated.
584 proc delete_files
{path_list
after} {
585 # Enable progress bar status updates
586 set status_bar_operation
[$::main_status \
592 set deletion_errors
[list]
600 $status_bar_operation \
604 # Helper function to delete a list of files in batches. Each call deletes one
605 # batch of files, and then schedules a call for the next batch after any UI
606 # messages have been processed.
607 proc delete_helper
{path_list path_index deletion_errors batch_size
\
608 status_bar_operation
after} {
611 set path_cnt
[llength $path_list]
613 set batch_remaining
$batch_size
615 while {$batch_remaining > 0} {
616 if {$path_index >= $path_cnt} { break }
618 set path
[lindex $path_list $path_index]
620 set deletion_failed
[catch {file delete
-- $path} deletion_error
]
622 if {$deletion_failed} {
623 lappend deletion_errors
[list "$deletion_error"]
625 remove_empty_directories
[file dirname
$path]
627 # Don't assume the deletion worked. Remove the file from
628 # the UI, but only if it no longer exists.
629 if {![path_exists
$path]} {
630 unset file_states
($path)
631 display_file
$path __
636 incr batch_remaining
-1
639 # Update the progress bar to indicate that this batch has been
640 # completed. The update will be visible when this procedure returns
641 # and allows the UI thread to process messages.
642 $status_bar_operation update $path_index $path_cnt
644 if {$path_index < $path_cnt} {
645 # The Tcler's Wiki lists this as the best practice for keeping
646 # a UI active and processing messages during a long-running
649 after idle
[list after 0 [list \
655 $status_bar_operation \
659 # Finish the status bar operation.
660 $status_bar_operation stop
662 # Report error, if any, based on how many deletions failed.
663 set deletion_error_cnt
[llength $deletion_errors]
665 if {($deletion_error_cnt > 0)
666 && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR
])} {
667 set error_text
[mc
"Encountered errors deleting files:\n"]
669 foreach deletion_error
$deletion_errors {
670 append error_text
"* [lindex $deletion_error 0]\n"
673 error_popup
$error_text
674 } elseif
{$deletion_error_cnt == $path_cnt} {
676 "None of the %d selected files could be deleted." \
679 } elseif
{$deletion_error_cnt > 1} {
681 "%d of the %d selected files could not be deleted." \
682 $deletion_error_cnt \
691 proc MAX_VERBOSE_FILES_IN_DELETION_ERROR
{} { return 10; }
693 # This function is from the TCL documentation:
695 # https://wiki.tcl-lang.org/page/file+exists
697 # [file exists] returns false if the path does exist but is a symlink to a path
698 # that doesn't exist. This proc returns true if the path exists, regardless of
699 # whether it is a symlink and whether it is broken.
700 proc path_exists
{name
} {
701 expr {![catch {file lstat
$name finfo
}]}
704 # Remove as many empty directories as we can starting at the specified path,
705 # walking up the directory tree. If we encounter a directory that is not
706 # empty, or if a directory deletion fails, then we stop the operation and
707 # return to the caller. Even if this procedure fails to delete any
708 # directories at all, it does not report failure.
709 proc remove_empty_directories
{directory_path
} {
710 set parent_path
[file dirname
$directory_path]
712 while {$parent_path != $directory_path} {
713 set contents
[glob -nocomplain -dir $directory_path *]
715 if {[llength $contents] > 0} { break }
716 if {[catch {file delete
-- $directory_path}]} { break }
718 set directory_path
$parent_path
719 set parent_path
[file dirname
$directory_path]
723 proc do_revert_selection
{} {
724 global current_diff_path selected_paths
726 if {[array size selected_paths
] > 0} {
728 [mc
"Reverting selected files"] \
729 [array names selected_paths
]
730 } elseif
{$current_diff_path ne
{}} {
732 [mc
"Reverting %s" [short_path
$current_diff_path]] \
733 [list $current_diff_path]
737 proc do_select_commit_type
{} {
738 global commit_type commit_type_is_amend
740 if {$commit_type_is_amend == 0
741 && [string match amend
* $commit_type]} {
743 } elseif
{$commit_type_is_amend == 1
744 && ![string match amend
* $commit_type]} {
747 # The amend request was rejected...
749 if {![string match amend
* $commit_type]} {
750 set commit_type_is_amend
0