Git fork
1# git-gui index (add/remove) support
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc _delete_indexlock {} {
5 if {[catch {file delete -- [gitdir index.lock]} err]} {
6 error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"]
7 }
8}
9
10proc close_and_unlock_index {fd after} {
11 if {![catch {_close_updateindex $fd} err]} {
12 unlock_index
13 uplevel #0 $after
14 } else {
15 rescan_on_error $err $after
16 }
17}
18
19proc _close_updateindex {fd} {
20 fconfigure $fd -blocking 1
21 close $fd
22}
23
24proc rescan_on_error {err {after {}}} {
25 set w .indexfried
26 Dialog $w
27 wm withdraw $w
28 wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]]
29 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
30 set s [mc "Updating the Git index failed. A rescan will be automatically started to resynchronize git-gui."]
31 text $w.msg -yscrollcommand [list $w.vs set] \
32 -width [string length $s] -relief flat \
33 -borderwidth 0 -highlightthickness 0 \
34 -background [get_bg_color $w]
35 $w.msg tag configure bold -font font_uibold -justify center
36 ttk::scrollbar $w.vs -command [list $w.msg yview]
37 $w.msg insert end $s bold \n\n$err {}
38 $w.msg configure -state disabled
39
40 ttk::button $w.continue \
41 -text [mc "Continue"] \
42 -command [list destroy $w]
43 ttk::button $w.unlock \
44 -text [mc "Unlock Index"] \
45 -command "destroy $w; _delete_indexlock"
46 grid $w.msg - $w.vs -sticky news
47 grid $w.unlock $w.continue - -sticky se -padx 2 -pady 2
48 grid columnconfigure $w 0 -weight 1
49 grid rowconfigure $w 0 -weight 1
50
51 wm protocol $w WM_DELETE_WINDOW update
52 bind $w.continue <Visibility> "
53 grab $w
54 focus %W
55 "
56 wm deiconify $w
57 tkwait window $w
58
59 $::main_status stop_all
60 unlock_index
61 rescan [concat $after {ui_ready;}] 0
62}
63
64proc update_indexinfo {msg path_list after} {
65 global update_index_cp
66
67 if {![lock_index update]} return
68
69 set update_index_cp 0
70 set path_list [lsort $path_list]
71 set total_cnt [llength $path_list]
72 set batch [expr {int($total_cnt * .01) + 1}]
73 if {$batch > 25} {set batch 25}
74
75 set status_bar_operation [$::main_status start $msg [mc "files"]]
76 set fd [git_write [list update-index -z --index-info]]
77 fconfigure $fd \
78 -blocking 0 \
79 -buffering full \
80 -buffersize 512 \
81 -translation binary
82 fileevent $fd writable [list \
83 write_update_indexinfo \
84 $fd \
85 $path_list \
86 $total_cnt \
87 $batch \
88 $status_bar_operation \
89 $after \
90 ]
91}
92
93proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \
94 after} {
95 global update_index_cp
96 global file_states current_diff_path
97
98 if {$update_index_cp >= $total_cnt} {
99 $status_bar_operation stop
100 close_and_unlock_index $fd $after
101 return
102 }
103
104 for {set i $batch} \
105 {$update_index_cp < $total_cnt && $i > 0} \
106 {incr i -1} {
107 set path [lindex $path_list $update_index_cp]
108 incr update_index_cp
109
110 set s $file_states($path)
111 switch -glob -- [lindex $s 0] {
112 A? {set new _O}
113 MT -
114 TM -
115 T_ {set new _T}
116 M? {set new _M}
117 TD -
118 D_ {set new _D}
119 D? {set new _?}
120 ?? {continue}
121 }
122 set info [lindex $s 2]
123 if {$info eq {}} continue
124
125 puts -nonewline $fd "$info\t[encoding convertto utf-8 $path]\0"
126 display_file $path $new
127 }
128
129 $status_bar_operation update $update_index_cp $total_cnt
130}
131
132proc update_index {msg path_list after} {
133 global update_index_cp
134
135 if {![lock_index update]} return
136
137 set update_index_cp 0
138 set path_list [lsort $path_list]
139 set total_cnt [llength $path_list]
140 set batch [expr {int($total_cnt * .01) + 1}]
141 if {$batch > 25} {set batch 25}
142
143 set status_bar_operation [$::main_status start $msg [mc "files"]]
144 set fd [git_write [list update-index --add --remove -z --stdin]]
145 fconfigure $fd \
146 -blocking 0 \
147 -buffering full \
148 -buffersize 512 \
149 -translation binary
150 fileevent $fd writable [list \
151 write_update_index \
152 $fd \
153 $path_list \
154 $total_cnt \
155 $batch \
156 $status_bar_operation \
157 $after \
158 ]
159}
160
161proc write_update_index {fd path_list total_cnt batch status_bar_operation \
162 after} {
163 global update_index_cp
164 global file_states current_diff_path
165
166 if {$update_index_cp >= $total_cnt} {
167 $status_bar_operation stop
168 close_and_unlock_index $fd $after
169 return
170 }
171
172 for {set i $batch} \
173 {$update_index_cp < $total_cnt && $i > 0} \
174 {incr i -1} {
175 set path [lindex $path_list $update_index_cp]
176 incr update_index_cp
177
178 switch -glob -- [lindex $file_states($path) 0] {
179 AD {set new __}
180 ?D {set new D_}
181 _O -
182 AT -
183 AM {set new A_}
184 TM -
185 MT -
186 _T {set new T_}
187 _U -
188 U? {
189 if {[file exists $path]} {
190 set new M_
191 } else {
192 set new D_
193 }
194 }
195 ?M {set new M_}
196 ?? {continue}
197 }
198 puts -nonewline $fd "[encoding convertto utf-8 $path]\0"
199 display_file $path $new
200 }
201
202 $status_bar_operation update $update_index_cp $total_cnt
203}
204
205proc checkout_index {msg path_list after capture_error} {
206 global update_index_cp
207
208 if {![lock_index update]} return
209
210 set update_index_cp 0
211 set path_list [lsort $path_list]
212 set total_cnt [llength $path_list]
213 set batch [expr {int($total_cnt * .01) + 1}]
214 if {$batch > 25} {set batch 25}
215
216 set status_bar_operation [$::main_status start $msg [mc "files"]]
217 set fd [git_write [list checkout-index \
218 --index \
219 --quiet \
220 --force \
221 -z \
222 --stdin \
223 ]]
224 fconfigure $fd \
225 -blocking 0 \
226 -buffering full \
227 -buffersize 512 \
228 -translation binary
229 fileevent $fd writable [list \
230 write_checkout_index \
231 $fd \
232 $path_list \
233 $total_cnt \
234 $batch \
235 $status_bar_operation \
236 $after \
237 $capture_error \
238 ]
239}
240
241proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \
242 after capture_error} {
243 global update_index_cp
244 global file_states current_diff_path
245
246 if {$update_index_cp >= $total_cnt} {
247 $status_bar_operation stop
248
249 # We do not unlock the index directly here because this
250 # operation expects to potentially run in parallel with file
251 # deletions scheduled by revert_helper. We're done with the
252 # update index, so we close it, but actually unlocking the index
253 # and dealing with potential errors is deferred to the chord
254 # body that runs when all async operations are completed.
255 #
256 # (See after_chord in revert_helper.)
257
258 if {[catch {_close_updateindex $fd} err]} {
259 uplevel #0 $capture_error [list $err]
260 }
261
262 uplevel #0 $after
263
264 return
265 }
266
267 for {set i $batch} \
268 {$update_index_cp < $total_cnt && $i > 0} \
269 {incr i -1} {
270 set path [lindex $path_list $update_index_cp]
271 incr update_index_cp
272 switch -glob -- [lindex $file_states($path) 0] {
273 U? {continue}
274 ?M -
275 ?T -
276 ?D {
277 puts -nonewline $fd "[encoding convertto utf-8 $path]\0"
278 display_file $path ?_
279 }
280 }
281 }
282
283 $status_bar_operation update $update_index_cp $total_cnt
284}
285
286proc unstage_helper {txt paths} {
287 global file_states current_diff_path
288
289 if {![lock_index begin-update]} return
290
291 set path_list [list]
292 set after {}
293 foreach path $paths {
294 switch -glob -- [lindex $file_states($path) 0] {
295 A? -
296 M? -
297 T? -
298 D? {
299 lappend path_list $path
300 if {$path eq $current_diff_path} {
301 set after {reshow_diff;}
302 }
303 }
304 }
305 }
306 if {$path_list eq {}} {
307 unlock_index
308 } else {
309 update_indexinfo \
310 $txt \
311 $path_list \
312 [concat $after {ui_ready;}]
313 }
314}
315
316proc do_unstage_selection {} {
317 global current_diff_path selected_paths
318
319 if {[array size selected_paths] > 0} {
320 unstage_helper \
321 [mc "Unstaging selected files from commit"] \
322 [array names selected_paths]
323 } elseif {$current_diff_path ne {}} {
324 unstage_helper \
325 [mc "Unstaging %s from commit" [short_path $current_diff_path]] \
326 [list $current_diff_path]
327 }
328}
329
330proc add_helper {txt paths} {
331 global file_states current_diff_path
332
333 if {![lock_index begin-update]} return
334
335 set path_list [list]
336 set after {}
337 foreach path $paths {
338 switch -glob -- [lindex $file_states($path) 0] {
339 _U -
340 U? {
341 if {$path eq $current_diff_path} {
342 unlock_index
343 merge_stage_workdir $path
344 return
345 }
346 }
347 _O -
348 ?M -
349 ?D -
350 ?T {
351 lappend path_list $path
352 if {$path eq $current_diff_path} {
353 set after {reshow_diff;}
354 }
355 }
356 }
357 }
358 if {$path_list eq {}} {
359 unlock_index
360 } else {
361 update_index \
362 $txt \
363 $path_list \
364 [concat $after {ui_status [mc "Ready to commit."];}]
365 }
366}
367
368proc do_add_selection {} {
369 global current_diff_path selected_paths
370
371 if {[array size selected_paths] > 0} {
372 add_helper \
373 [mc "Adding selected files"] \
374 [array names selected_paths]
375 } elseif {$current_diff_path ne {}} {
376 add_helper \
377 [mc "Adding %s" [short_path $current_diff_path]] \
378 [list $current_diff_path]
379 }
380}
381
382proc do_add_all {} {
383 global file_states
384
385 set paths [list]
386 set untracked_paths [list]
387 foreach path [array names file_states] {
388 switch -glob -- [lindex $file_states($path) 0] {
389 U? {continue}
390 ?M -
391 ?T -
392 ?D {lappend paths $path}
393 ?O {lappend untracked_paths $path}
394 }
395 }
396 if {[llength $untracked_paths]} {
397 set reply 0
398 switch -- [get_config gui.stageuntracked] {
399 no {
400 set reply 0
401 }
402 yes {
403 set reply 1
404 }
405 ask -
406 default {
407 set reply [ask_popup [mc "Stage %d untracked files?" \
408 [llength $untracked_paths]]]
409 }
410 }
411 if {$reply} {
412 set paths [concat $paths $untracked_paths]
413 }
414 }
415 add_helper [mc "Adding all changed files"] $paths
416}
417
418# Copied from TclLib package "lambda".
419proc lambda {arguments body args} {
420 return [list ::apply [list $arguments $body] {*}$args]
421}
422
423proc revert_helper {txt paths} {
424 global file_states current_diff_path
425
426 if {![lock_index begin-update]} return
427
428 # Workaround for Tcl < 9.0: chord namespaces are not obeyed and
429 # operated in the global namespace. This clears an error that could
430 # have been left over from a previous operation.
431 set ::err {}
432
433 # Common "after" functionality that waits until multiple asynchronous
434 # operations are complete (by waiting for them to activate their notes
435 # on the chord).
436 #
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 {[info exists err] && [string trim $err] ne ""} {
441 rescan_on_error $err
442 } else {
443 unlock_index
444 if {$should_reshow_diff} { reshow_diff }
445 ui_ready
446 }
447 }]
448
449 $after_chord eval { set should_reshow_diff 0 }
450
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 \
454 {chord error} \
455 { $chord eval [list set err $error] } \
456 $after_chord]
457
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]
463
464 set path_list [list]
465 set untracked_list [list]
466
467 foreach path $paths {
468 switch -glob -- [lindex $file_states($path) 0] {
469 U? {continue}
470 ?O {
471 lappend untracked_list $path
472 }
473 ?M -
474 ?T -
475 ?D {
476 lappend path_list $path
477 if {$path eq $current_diff_path} {
478 $after_chord eval { set should_reshow_diff 1 }
479 }
480 }
481 }
482 }
483
484 set path_cnt [llength $path_list]
485 set untracked_cnt [llength $untracked_list]
486
487 # Asynchronous operation: revert changes by checking them out afresh
488 # from the index.
489 if {$path_cnt > 0} {
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).
495 #
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
499 # translation.
500 #
501 if {$path_cnt == 1} {
502 set query [mc \
503 "Revert changes in file %s?" \
504 [short_path [lindex $path_list]] \
505 ]
506 } else {
507 set query [mc \
508 "Revert changes in these %i files?" \
509 $path_cnt]
510 }
511
512 set reply [tk_dialog \
513 .confirm_revert \
514 "[appname] ([reponame])" \
515 "$query
516
517[mc "Any unstaged changes will be permanently lost by the revert."]" \
518 question \
519 1 \
520 [mc "Do Nothing"] \
521 [mc "Revert Changes"] \
522 ]
523
524 if {$reply == 1} {
525 set note [$after_chord add_note]
526 checkout_index \
527 $txt \
528 $path_list \
529 [list $note activate] \
530 $capture_error
531 }
532 }
533
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.
538 #
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
542 # translation.
543 #
544 if {$untracked_cnt == 1} {
545 set query [mc \
546 "Delete untracked file %s?" \
547 [short_path [lindex $untracked_list]] \
548 ]
549 } else {
550 set query [mc \
551 "Delete these %i untracked files?" \
552 $untracked_cnt \
553 ]
554 }
555
556 set reply [tk_dialog \
557 .confirm_revert \
558 "[appname] ([reponame])" \
559 "$query
560
561[mc "Files will be permanently deleted."]" \
562 question \
563 1 \
564 [mc "Do Nothing"] \
565 [mc "Delete Files"] \
566 ]
567
568 if {$reply == 1} {
569 $after_chord eval { set should_reshow_diff 1 }
570
571 set note [$after_chord add_note]
572 delete_files $untracked_list [list $note activate]
573 }
574 }
575
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
580}
581
582# Delete all of the specified files, performing deletion in batches to allow the
583# UI to remain responsive and updated.
584proc delete_files {path_list after} {
585 # Enable progress bar status updates
586 set status_bar_operation [$::main_status \
587 start \
588 [mc "Deleting"] \
589 [mc "files"]]
590
591 set path_index 0
592 set deletion_errors [list]
593 set batch_size 50
594
595 delete_helper \
596 $path_list \
597 $path_index \
598 $deletion_errors \
599 $batch_size \
600 $status_bar_operation \
601 $after
602}
603
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.
607proc delete_helper {path_list path_index deletion_errors batch_size \
608 status_bar_operation after} {
609 global file_states
610
611 set path_cnt [llength $path_list]
612
613 set batch_remaining $batch_size
614
615 while {$batch_remaining > 0} {
616 if {$path_index >= $path_cnt} { break }
617
618 set path [lindex $path_list $path_index]
619
620 set deletion_failed [catch {file delete -- $path} deletion_error]
621
622 if {$deletion_failed} {
623 lappend deletion_errors [list "$deletion_error"]
624 } else {
625 remove_empty_directories [file dirname $path]
626
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 __
632 }
633 }
634
635 incr path_index 1
636 incr batch_remaining -1
637 }
638
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
643
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
647 # operation.
648
649 after idle [list after 0 [list \
650 delete_helper \
651 $path_list \
652 $path_index \
653 $deletion_errors \
654 $batch_size \
655 $status_bar_operation \
656 $after
657 ]]
658 } else {
659 # Finish the status bar operation.
660 $status_bar_operation stop
661
662 # Report error, if any, based on how many deletions failed.
663 set deletion_error_cnt [llength $deletion_errors]
664
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"]
668
669 foreach deletion_error $deletion_errors {
670 append error_text "* [lindex $deletion_error 0]\n"
671 }
672
673 error_popup $error_text
674 } elseif {$deletion_error_cnt == $path_cnt} {
675 error_popup [mc \
676 "None of the %d selected files could be deleted." \
677 $path_cnt \
678 ]
679 } elseif {$deletion_error_cnt > 1} {
680 error_popup [mc \
681 "%d of the %d selected files could not be deleted." \
682 $deletion_error_cnt \
683 $path_cnt \
684 ]
685 }
686
687 uplevel #0 $after
688 }
689}
690
691proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; }
692
693# This function is from the TCL documentation:
694#
695# https://wiki.tcl-lang.org/page/file+exists
696#
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.
700proc path_exists {name} {
701 expr {![catch {file lstat $name finfo}]}
702}
703
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.
709proc remove_empty_directories {directory_path} {
710 set parent_path [file dirname $directory_path]
711
712 while {$parent_path != $directory_path} {
713 set contents [glob -nocomplain -dir $directory_path *]
714
715 if {[llength $contents] > 0} { break }
716 if {[catch {file delete -- $directory_path}]} { break }
717
718 set directory_path $parent_path
719 set parent_path [file dirname $directory_path]
720 }
721}
722
723proc do_revert_selection {} {
724 global current_diff_path selected_paths
725
726 if {[array size selected_paths] > 0} {
727 revert_helper \
728 [mc "Reverting selected files"] \
729 [array names selected_paths]
730 } elseif {$current_diff_path ne {}} {
731 revert_helper \
732 [mc "Reverting %s" [short_path $current_diff_path]] \
733 [list $current_diff_path]
734 }
735}
736
737proc do_select_commit_type {} {
738 global commit_type commit_type_is_amend
739
740 if {$commit_type_is_amend == 0
741 && [string match amend* $commit_type]} {
742 create_new_commit
743 } elseif {$commit_type_is_amend == 1
744 && ![string match amend* $commit_type]} {
745 load_last_commit
746
747 # The amend request was rejected...
748 #
749 if {![string match amend* $commit_type]} {
750 set commit_type_is_amend 0
751 }
752 }
753}