diff options
Diffstat (limited to 'git-gui/lib')
-rw-r--r-- | git-gui/lib/blame.tcl | 300 | ||||
-rw-r--r-- | git-gui/lib/browser.tcl | 2 | ||||
-rw-r--r-- | git-gui/lib/choose_repository.tcl | 22 | ||||
-rw-r--r-- | git-gui/lib/commit.tcl | 11 | ||||
-rw-r--r-- | git-gui/lib/diff.tcl | 186 | ||||
-rw-r--r-- | git-gui/lib/encoding.tcl | 250 | ||||
-rw-r--r-- | git-gui/lib/index.tcl | 21 | ||||
-rw-r--r-- | git-gui/lib/merge.tcl | 1 | ||||
-rw-r--r-- | git-gui/lib/mergetool.tcl | 393 | ||||
-rw-r--r-- | git-gui/lib/option.tcl | 49 | ||||
-rw-r--r-- | git-gui/lib/remote.tcl | 188 | ||||
-rw-r--r-- | git-gui/lib/remote_add.tcl | 191 | ||||
-rw-r--r-- | git-gui/lib/remote_branch_delete.tcl | 6 | ||||
-rw-r--r-- | git-gui/lib/search.tcl | 198 | ||||
-rw-r--r-- | git-gui/lib/spellcheck.tcl | 1 | ||||
-rw-r--r-- | git-gui/lib/sshkey.tcl | 126 | ||||
-rw-r--r-- | git-gui/lib/tools.tcl | 159 | ||||
-rw-r--r-- | git-gui/lib/tools_dlg.tcl | 421 | ||||
-rw-r--r-- | git-gui/lib/transport.tcl | 2 |
19 files changed, 2338 insertions, 189 deletions
diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl index b6e42cbc8f..c1cd7f3b92 100644 --- a/git-gui/lib/blame.tcl +++ b/git-gui/lib/blame.tcl @@ -21,9 +21,11 @@ field w_amov ; # text column: annotations + move tracking field w_asim ; # text column: annotations (simple computation) field w_file ; # text column: actual file data field w_cviewer ; # pane showing commit message +field finder ; # find mini-dialog frame field status ; # status mega-widget instance field old_height ; # last known height of $w.file_pane + # Tk UI colors # variable active_color #c0edc5 @@ -58,8 +60,8 @@ field tooltip_t {} ; # Text widget in $tooltip_wm field tooltip_timer {} ; # Current timer event for our tooltip field tooltip_commit {} ; # Commit(s) in tooltip -constructor new {i_commit i_path} { - global cursor_ptr +constructor new {i_commit i_path i_jump} { + global cursor_ptr M1B M1T have_tk85 variable active_color variable group_colors @@ -69,6 +71,8 @@ constructor new {i_commit i_path} { make_toplevel top w wm title $top [append "[appname] ([reponame]): " [mc "File Viewer"]] + set font_w [font measure font_diff "0"] + frame $w.header -background gold label $w.header.commit_l \ -text [mc "Commit:"] \ @@ -114,9 +118,9 @@ constructor new {i_commit i_path} { pack $w_path -fill x -side right pack $w.header.path_l -side right - panedwindow $w.file_pane -orient vertical - frame $w.file_pane.out - frame $w.file_pane.cm + panedwindow $w.file_pane -orient vertical -borderwidth 0 -sashwidth 3 + frame $w.file_pane.out -relief flat -borderwidth 1 + frame $w.file_pane.cm -relief sunken -borderwidth 1 $w.file_pane add $w.file_pane.out \ -sticky nsew \ -minsize 100 \ @@ -197,6 +201,11 @@ constructor new {i_commit i_path} { -width 80 \ -xscrollcommand [list $w.file_pane.out.sbx set] \ -font font_diff + if {$have_tk85} { + $w_file configure -inactiveselectbackground darkblue + } + $w_file tag conf found \ + -background yellow set w_columns [list $w_amov $w_asim $w_line $w_file] @@ -217,6 +226,11 @@ constructor new {i_commit i_path} { -weight 1 grid rowconfigure $w.file_pane.out 0 -weight 1 + set finder [::searchbar::new \ + $w.file_pane.out.ff $w_file \ + -column [expr {[llength $w_columns] - 1}] \ + ] + set w_cviewer $w.file_pane.cm.t text $w_cviewer \ -background white \ @@ -256,18 +270,41 @@ constructor new {i_commit i_path} { $w.ctxm add command \ -label [mc "Copy Commit"] \ -command [cb _copycommit] + $w.ctxm add separator + $w.ctxm add command \ + -label [mc "Find Text..."] \ + -accelerator F7 \ + -command [list searchbar::show $finder] + menu $w.ctxm.enc + build_encoding_menu $w.ctxm.enc [cb _setencoding] + $w.ctxm add cascade \ + -label [mc "Encoding"] \ + -menu $w.ctxm.enc $w.ctxm add command \ -label [mc "Do Full Copy Detection"] \ -command [cb _fullcopyblame] + $w.ctxm add separator + $w.ctxm add command \ + -label [mc "Show History Context"] \ + -command [cb _gitkcommit] + $w.ctxm add command \ + -label [mc "Blame Parent Commit"] \ + -command [cb _blameparent] foreach i $w_columns { for {set g 0} {$g < [llength $group_colors]} {incr g} { $i tag conf color$g -background [lindex $group_colors $g] } + if {$i eq $w_file} { + $w_file tag raise found + } + $i tag raise sel + $i conf -cursor $cursor_ptr - $i conf -yscrollcommand [list many2scrollbar \ - $w_columns yview $w.file_pane.out.sby] + $i conf -yscrollcommand \ + "[list ::searchbar::scrolled $finder] + [list many2scrollbar $w_columns yview $w.file_pane.out.sby]" bind $i <Button-1> " [cb _hide_tooltip] [cb _click $i @%x,%y] @@ -284,7 +321,7 @@ constructor new {i_commit i_path} { tk_popup $w.ctxm %X %Y " bind $i <Shift-Tab> "[list focus $w_cviewer];break" - bind $i <Tab> "[list focus $w_cviewer];break" + bind $i <Tab> "[cb _focus_search $w_cviewer];break" } foreach i [concat $w_columns $w_cviewer] { @@ -300,10 +337,15 @@ constructor new {i_commit i_path} { bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break} } - bind $w_cviewer <Shift-Tab> "[list focus $w_file];break" + bind $w_cviewer <Shift-Tab> "[cb _focus_search $w_file];break" bind $w_cviewer <Tab> "[list focus $w_file];break" - bind $w_cviewer <Button-1> [list focus $w_cviewer] - bind $w_file <Visibility> [list focus $w_file] + bind $w_cviewer <Button-1> [list focus $w_cviewer] + bind $w_file <Visibility> [cb _focus_search $w_file] + bind $top <F7> [list searchbar::show $finder] + bind $top <Escape> [list searchbar::hide $finder] + bind $top <F3> [list searchbar::find_next $finder] + bind $top <Shift-F3> [list searchbar::find_prev $finder] + catch { bind $top <Shift-Key-XF86_Switch_VT_3> [list searchbar::find_prev $finder] } grid configure $w.header -sticky ew grid configure $w.file_pane -sticky nsew @@ -315,9 +357,14 @@ constructor new {i_commit i_path} { set req_w [winfo reqwidth $top] set req_h [winfo reqheight $top] - set scr_h [expr {[winfo screenheight $top] - 100}] - if {$req_w < 600} {set req_w 600} + set scr_w [expr {[winfo screenwidth $top] - 40}] + set scr_h [expr {[winfo screenheight $top] - 120}] + set opt_w [expr {$font_w * (80 + 5*3 + 3)}] + if {$req_w < $opt_w} {set req_w $opt_w} + if {$req_w > $scr_w} {set req_w $scr_w} + set opt_h [expr {$req_w*4/3}] if {$req_h < $scr_h} {set req_h $scr_h} + if {$req_h > $opt_h} {set req_h $opt_h} set g "${req_w}x${req_h}" wm geometry $top $g update @@ -325,14 +372,29 @@ constructor new {i_commit i_path} { set old_height [winfo height $w.file_pane] $w.file_pane sash place 0 \ [lindex [$w.file_pane sash coord 0] 0] \ - [expr {int($old_height * 0.70)}] + [expr {int($old_height * 0.80)}] bind $w.file_pane <Configure> \ "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}" wm protocol $top WM_DELETE_WINDOW "destroy $top" - bind $top <Destroy> [cb _kill] + bind $top <Destroy> [cb _handle_destroy %W] - _load $this {} + _load $this $i_jump +} + +method _focus_search {win} { + if {[searchbar::visible $finder]} { + focus [searchbar::editor $finder] + } else { + focus $win + } +} + +method _handle_destroy {win} { + if {$win eq $w} { + _kill $this + delete_this + } } method _kill {} { @@ -393,7 +455,10 @@ method _load {jump} { } else { set fd [git_read cat-file blob "$commit:$path"] } - fconfigure $fd -blocking 0 -translation lf -encoding binary + fconfigure $fd \ + -blocking 0 \ + -translation lf \ + -encoding [get_path_encoding $path] fileevent $fd readable [cb _read_file $fd $jump] set current_fd $fd } @@ -494,7 +559,7 @@ method _read_file {fd jump} { } ifdeleted { catch {close $fd} } method _exec_blame {cur_w cur_d options cur_s} { - lappend options --incremental + lappend options --incremental --encoding=utf-8 if {$commit eq {}} { lappend options --contents $path } else { @@ -502,7 +567,7 @@ method _exec_blame {cur_w cur_d options cur_s} { } lappend options -- $path set fd [eval git_read --nice blame $options] - fconfigure $fd -blocking 0 -translation lf -encoding binary + fconfigure $fd -blocking 0 -translation lf -encoding utf-8 fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d] set current_fd $fd set blame_lines 0 @@ -782,24 +847,42 @@ method _click {cur_w pos} { _showcommit $this $cur_w $lno } +method _setencoding {enc} { + force_path_encoding $path $enc + _load $this [list \ + $highlight_column \ + $highlight_line \ + [lindex [$w_file xview] 0] \ + [lindex [$w_file yview] 0] \ + ] +} + method _load_commit {cur_w cur_d pos} { upvar #0 $cur_d line_data set lno [lindex [split [$cur_w index $pos] .] 0] set dat [lindex $line_data $lno] if {$dat ne {}} { - lappend history [list \ - $commit $path \ - $highlight_column \ - $highlight_line \ - [lindex [$w_file xview] 0] \ - [lindex [$w_file yview] 0] \ - ] - set commit [lindex $dat 0] - set path [lindex $dat 1] - _load $this [list [lindex $dat 2]] + _load_new_commit $this \ + [lindex $dat 0] \ + [lindex $dat 1] \ + [list [lindex $dat 2]] } } +method _load_new_commit {new_commit new_path jump} { + lappend history [list \ + $commit $path \ + $highlight_column \ + $highlight_line \ + [lindex [$w_file xview] 0] \ + [lindex [$w_file yview] 0] \ + ] + + set commit $new_commit + set path $new_path + _load $this $jump +} + method _showcommit {cur_w lno} { global repo_config variable active_color @@ -832,6 +915,10 @@ method _showcommit {cur_w lno} { foreach i $w_columns { $i tag conf g$cmit -background $active_color $i tag raise g$cmit + if {$i eq $w_file} { + $w_file tag raise found + } + $i tag raise sel } set author_name {} @@ -867,12 +954,6 @@ method _showcommit {cur_w lno} { set enc [tcl_encoding $enc] if {$enc ne {}} { set msg [encoding convertfrom $enc $msg] - set author_name [encoding convertfrom $enc $author_name] - set committer_name [encoding convertfrom $enc $committer_name] - set header($cmit,author) $author_name - set header($cmit,committer) $committer_name - set header($cmit,summary) \ - [encoding convertfrom $enc $header($cmit,summary)] } set msg [string trim $msg] } @@ -905,10 +986,14 @@ method _showcommit {cur_w lno} { } } -method _copycommit {} { +method _get_click_amov_info {} { set pos @$::cursorX,$::cursorY set lno [lindex [split [$::cursorW index $pos] .] 0] - set dat [lindex $amov_data $lno] + return [lindex $amov_data $lno] +} + +method _copycommit {} { + set dat [_get_click_amov_info $this] if {$dat ne {}} { clipboard clear clipboard append \ @@ -918,6 +1003,147 @@ method _copycommit {} { } } +method _format_offset_date {base offset} { + set exval [expr {$base + $offset*24*60*60}] + return [clock format $exval -format {%Y-%m-%d}] +} + +method _gitkcommit {} { + global nullid + + set dat [_get_click_amov_info $this] + if {$dat ne {}} { + set cmit [lindex $dat 0] + + # If the line belongs to the working copy, use HEAD instead + if {$cmit eq $nullid} { + if {[catch {set cmit [git rev-parse --verify HEAD]} err]} { + error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"] + return; + } + } + + set radius [get_config gui.blamehistoryctx] + set cmdline [list --select-commit=$cmit] + + if {$radius > 0} { + set author_time {} + set committer_time {} + + catch {set author_time $header($cmit,author-time)} + catch {set committer_time $header($cmit,committer-time)} + + if {$committer_time eq {}} { + set committer_time $author_time + } + + set after_time [_format_offset_date $this $committer_time [expr {-$radius}]] + set before_time [_format_offset_date $this $committer_time $radius] + + lappend cmdline --after=$after_time --before=$before_time + } + + lappend cmdline $cmit + + set base_rev "HEAD" + if {$commit ne {}} { + set base_rev $commit + } + + if {$base_rev ne $cmit} { + lappend cmdline $base_rev + } + + do_gitk $cmdline + } +} + +method _blameparent {} { + global nullid + + set dat [_get_click_amov_info $this] + if {$dat ne {}} { + set cmit [lindex $dat 0] + set new_path [lindex $dat 1] + + # Allow using Blame Parent on lines modified in the working copy + if {$cmit eq $nullid} { + set parent_ref "HEAD" + } else { + set parent_ref "$cmit^" + } + if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} { + error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"] + return; + } + + _kill $this + + # Generate a diff between the commit and its parent, + # and use the hunks to update the line number. + # Request zero context to simplify calculations. + if {$cmit eq $nullid} { + set diffcmd [list diff-index --unified=0 $cparent -- $new_path] + } else { + set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path] + } + if {[catch {set fd [eval git_read $diffcmd]} err]} { + $status stop [mc "Unable to display parent"] + error_popup [strcat [mc "Error loading diff:"] "\n\n$err"] + return + } + + set r_orig_line [lindex $dat 2] + + fconfigure $fd \ + -blocking 0 \ + -encoding binary \ + -translation binary + fileevent $fd readable [cb _read_diff_load_commit \ + $fd $cparent $new_path $r_orig_line] + set current_fd $fd + } +} + +method _read_diff_load_commit {fd cparent new_path tline} { + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + while {[gets $fd line] >= 0} { + if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \ + old_line osz old_size new_line nsz new_size]} { + + if {$osz eq {}} { set old_size 1 } + if {$nsz eq {}} { set new_size 1 } + + if {$new_line <= $tline} { + if {[expr {$new_line + $new_size}] > $tline} { + # Target line within the hunk + set line_shift [expr { + ($new_size-$old_size)*($tline-$new_line)/$new_size + }] + } else { + set line_shift [expr {$new_size-$old_size}] + } + + set r_orig_line [expr {$r_orig_line - $line_shift}] + } + } + } + + if {[eof $fd]} { + close $fd; + set current_fd {} + + _load_new_commit $this \ + $cparent \ + $new_path \ + [list $r_orig_line] + } +} ifdeleted { catch {close $fd} } + method _show_tooltip {cur_w pos} { if {$tooltip_wm ne {}} { _open_tooltip $this $cur_w diff --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl index ab470d1264..0410cc68df 100644 --- a/git-gui/lib/browser.tcl +++ b/git-gui/lib/browser.tcl @@ -151,7 +151,7 @@ method _enter {} { append p [lindex $n 1] } append p $name - blame::new $browser_commit $p + blame::new $browser_commit $p {} } } } diff --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl index 3180786158..f9ff62a3b2 100644 --- a/git-gui/lib/choose_repository.tcl +++ b/git-gui/lib/choose_repository.tcl @@ -43,12 +43,18 @@ constructor pick {} { $w.mbar.apple add command \ -label [mc "About %s" [appname]] \ -command do_about + $w.mbar.apple add command \ + -label [mc "Show SSH Key"] \ + -command do_ssh_key } else { $w.mbar add cascade -label [mc Help] -menu $w.mbar.help menu $w.mbar.help $w.mbar.help add command \ -label [mc "About %s" [appname]] \ -command do_about + $w.mbar.help add command \ + -label [mc "Show SSH Key"] \ + -command do_ssh_key } wm protocol $top WM_DELETE_WINDOW exit @@ -381,7 +387,8 @@ method _do_new {} { label $w_body.where.l -text [mc "Directory:"] entry $w_body.where.t \ -textvariable @local_path \ - -font font_diff \ + -borderwidth 1 \ + -relief sunken \ -width 50 button $w_body.where.b \ -text [mc "Browse"] \ @@ -463,20 +470,22 @@ method _do_clone {} { frame $w_body.args pack $args -fill both - label $args.origin_l -text [mc "URL:"] + label $args.origin_l -text [mc "Source Location:"] entry $args.origin_t \ -textvariable @origin_url \ - -font font_diff \ + -borderwidth 1 \ + -relief sunken \ -width 50 button $args.origin_b \ -text [mc "Browse"] \ -command [cb _open_origin] grid $args.origin_l $args.origin_t $args.origin_b -sticky ew - label $args.where_l -text [mc "Directory:"] + label $args.where_l -text [mc "Target Directory:"] entry $args.where_t \ -textvariable @local_path \ - -font font_diff \ + -borderwidth 1 \ + -relief sunken \ -width 50 button $args.where_b \ -text [mc "Browse"] \ @@ -979,7 +988,8 @@ method _do_open {} { label $w_body.where.l -text [mc "Repository:"] entry $w_body.where.t \ -textvariable @local_path \ - -font font_diff \ + -borderwidth 1 \ + -relief sunken \ -width 50 button $w_body.where.b \ -text [mc "Browse"] \ diff --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl index 40a7103557..334514996a 100644 --- a/git-gui/lib/commit.tcl +++ b/git-gui/lib/commit.tcl @@ -149,7 +149,9 @@ The rescan will be automatically started now. _? {continue} A? - D? - + T_ - M? {set files_ready 1} + _U - U? { error_popup [mc "Unmerged files cannot be committed. @@ -166,7 +168,7 @@ File %s cannot be committed by this program. } } } - if {!$files_ready && ![string match *merge $curType]} { + if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} { info_popup [mc "No changes to commit. You must stage at least 1 file before you can commit. @@ -175,6 +177,8 @@ You must stage at least 1 file before you can commit. return } + if {[is_enabled nocommitmsg]} { do_quit 0 } + # -- A message is required. # set msg [string trim [$ui_comm get 1.0 end]] @@ -210,6 +214,8 @@ A good commit message has the following format: puts $msg_wt $msg close $msg_wt + if {[is_enabled nocommit]} { do_quit 0 } + # -- Run the pre-commit hook. # set fd_ph [githook_read pre-commit] @@ -408,7 +414,7 @@ A rescan will be automatically started now. set ::GITGUI_BCK_exists 0 } - if {[is_enabled singlecommit]} do_quit + if {[is_enabled singlecommit]} { do_quit 0 } # -- Update in memory status # @@ -428,6 +434,7 @@ A rescan will be automatically started now. __ - A_ - M_ - + T_ - D_ { unset file_states($path) catch {unset selected_paths($path)} diff --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl index 1970b601e1..bbbf15c875 100644 --- a/git-gui/lib/diff.tcl +++ b/git-gui/lib/diff.tcl @@ -16,7 +16,7 @@ proc clear_diff {} { $ui_workdir tag remove in_diff 0.0 end } -proc reshow_diff {} { +proc reshow_diff {{after {}}} { global file_states file_lists global current_diff_path current_diff_side global ui_diff @@ -24,13 +24,28 @@ proc reshow_diff {} { set p $current_diff_path if {$p eq {}} { # No diff is being shown. - } elseif {$current_diff_side eq {} - || [catch {set s $file_states($p)}] - || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} { + } elseif {$current_diff_side eq {}} { clear_diff + } elseif {[catch {set s $file_states($p)}] + || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} { + + if {[find_next_diff $current_diff_side $p {} {[^O]}]} { + next_diff $after + } else { + clear_diff + } } else { set save_pos [lindex [$ui_diff yview] 0] - show_diff $p $current_diff_side {} $save_pos + show_diff $p $current_diff_side {} $save_pos $after + } +} + +proc force_diff_encoding {enc} { + global current_diff_path + + if {$current_diff_path ne {}} { + force_path_encoding $current_diff_path $enc + reshow_diff } } @@ -54,11 +69,12 @@ A rescan will be automatically started to find other files which may have the sa rescan ui_ready 0 } -proc show_diff {path w {lno {}} {scroll_pos {}}} { +proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} { global file_states file_lists - global is_3way_diff diff_active repo_config + global is_3way_diff is_conflict_diff diff_active repo_config global ui_diff ui_index ui_workdir global current_diff_path current_diff_side current_diff_header + global current_diff_queue if {$diff_active || ![lock_index read]} return @@ -71,21 +87,84 @@ proc show_diff {path w {lno {}} {scroll_pos {}}} { } if {$lno >= 1} { $w tag add in_diff $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 } set s $file_states($path) set m [lindex $s 0] - set is_3way_diff 0 - set diff_active 1 + set is_conflict_diff 0 set current_diff_path $path set current_diff_side $w - set current_diff_header {} + set current_diff_queue {} ui_status [mc "Loading diff of %s..." [escape_path $path]] + set cont_info [list $scroll_pos $callback] + + if {[string first {U} $m] >= 0} { + merge_load_stages $path [list show_unmerged_diff $cont_info] + } elseif {$m eq {_O}} { + show_other_diff $path $w $m $cont_info + } else { + start_show_diff $cont_info + } +} + +proc show_unmerged_diff {cont_info} { + global current_diff_path current_diff_side + global merge_stages ui_diff is_conflict_diff + global current_diff_queue + + if {$merge_stages(2) eq {}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "LOCAL: deleted\nREMOTE:\n"] d======= \ + [list ":1:$current_diff_path" ":3:$current_diff_path"]] + } elseif {$merge_stages(3) eq {}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "REMOTE: deleted\nLOCAL:\n"] d======= \ + [list ":1:$current_diff_path" ":2:$current_diff_path"]] + } elseif {[lindex $merge_stages(1) 0] eq {120000} + || [lindex $merge_stages(2) 0] eq {120000} + || [lindex $merge_stages(3) 0] eq {120000}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "LOCAL:\n"] d======= \ + [list ":1:$current_diff_path" ":2:$current_diff_path"]] + lappend current_diff_queue \ + [list [mc "REMOTE:\n"] d======= \ + [list ":1:$current_diff_path" ":3:$current_diff_path"]] + } else { + start_show_diff $cont_info + return + } + + advance_diff_queue $cont_info +} + +proc advance_diff_queue {cont_info} { + global current_diff_queue ui_diff + + set item [lindex $current_diff_queue 0] + set current_diff_queue [lrange $current_diff_queue 1 end] + + $ui_diff conf -state normal + $ui_diff insert end [lindex $item 0] [lindex $item 1] + $ui_diff conf -state disabled + + start_show_diff $cont_info [lindex $item 2] +} + +proc show_other_diff {path w m cont_info} { + global file_states file_lists + global is_3way_diff diff_active repo_config + global ui_diff ui_index ui_workdir + global current_diff_path current_diff_side current_diff_header + # - Git won't give us the diff, there's nothing to compare to! # if {$m eq {_O}} { - set max_sz [expr {128 * 1024}] + set max_sz 100000 set type unknown if {[catch { set type [file type $path] @@ -101,7 +180,9 @@ proc show_diff {path w {lno {}} {scroll_pos {}}} { } file { set fd [open $path r] - fconfigure $fd -eofchar {} + fconfigure $fd \ + -eofchar {} \ + -encoding [get_path_encoding $path] set content [read $fd $max_sz] close $fd set sz [file size $path] @@ -137,36 +218,57 @@ proc show_diff {path w {lno {}} {scroll_pos {}}} { d_@ } else { if {$sz > $max_sz} { - $ui_diff insert end \ -"* Untracked file is $sz bytes. -* Showing only first $max_sz bytes. -" d_@ + $ui_diff insert end [mc \ +"* Untracked file is %d bytes. +* Showing only first %d bytes. +" $sz $max_sz] d_@ } $ui_diff insert end $content if {$sz > $max_sz} { - $ui_diff insert end " -* Untracked file clipped here by [appname]. + $ui_diff insert end [mc " +* Untracked file clipped here by %s. * To see the entire file, use an external editor. -" d_@ +" [appname]] d_@ } } $ui_diff conf -state disabled set diff_active 0 unlock_index + set scroll_pos [lindex $cont_info 0] if {$scroll_pos ne {}} { update $ui_diff yview moveto $scroll_pos } ui_ready + set callback [lindex $cont_info 1] + if {$callback ne {}} { + eval $callback + } return } +} + +proc start_show_diff {cont_info {add_opts {}}} { + global file_states file_lists + global is_3way_diff diff_active repo_config + global ui_diff ui_index ui_workdir + global current_diff_path current_diff_side current_diff_header + + set path $current_diff_path + set w $current_diff_side + + set s $file_states($path) + set m [lindex $s 0] + set is_3way_diff 0 + set diff_active 1 + set current_diff_header {} set cmd [list] if {$w eq $ui_index} { lappend cmd diff-index lappend cmd --cached } elseif {$w eq $ui_workdir} { - if {[string index $m 0] eq {U}} { + if {[string first {U} $m] >= 0} { lappend cmd diff } else { lappend cmd diff-files @@ -181,8 +283,12 @@ proc show_diff {path w {lno {}} {scroll_pos {}}} { if {$w eq $ui_index} { lappend cmd [PARENT] } - lappend cmd -- - lappend cmd $path + if {$add_opts ne {}} { + eval lappend cmd $add_opts + } else { + lappend cmd -- + lappend cmd $path + } if {[catch {set fd [eval git_read --nice $cmd]} err]} { set diff_active 0 @@ -195,14 +301,15 @@ proc show_diff {path w {lno {}} {scroll_pos {}}} { set ::current_diff_inheader 1 fconfigure $fd \ -blocking 0 \ - -encoding binary \ - -translation binary - fileevent $fd readable [list read_diff $fd $scroll_pos] + -encoding [get_path_encoding $path] \ + -translation lf + fileevent $fd readable [list read_diff $fd $cont_info] } -proc read_diff {fd scroll_pos} { +proc read_diff {fd cont_info} { global ui_diff diff_active - global is_3way_diff current_diff_header + global is_3way_diff is_conflict_diff current_diff_header + global current_diff_queue $ui_diff conf -state normal while {[gets $fd line] >= 0} { @@ -249,6 +356,7 @@ proc read_diff {fd scroll_pos} { {--} {set tags d_--} {++} { if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} { + set is_conflict_diff 1 set line [string replace $line 0 1 { }] set tags d$op } else { @@ -268,7 +376,7 @@ proc read_diff {fd scroll_pos} { {-} {set tags d_-} {+} { if {[regexp {^\+([<>]{7} |={7})} $line _g op]} { - set line [string replace $line 0 0 { }] + set is_conflict_diff 1 set tags d$op } else { set tags d_+ @@ -290,8 +398,15 @@ proc read_diff {fd scroll_pos} { if {[eof $fd]} { close $fd + + if {$current_diff_queue ne {}} { + advance_diff_queue $cont_info + return + } + set diff_active 0 unlock_index + set scroll_pos [lindex $cont_info 0] if {$scroll_pos ne {}} { update $ui_diff yview moveto $scroll_pos @@ -301,6 +416,10 @@ proc read_diff {fd scroll_pos} { if {[$ui_diff index end] eq {2.0}} { handle_empty_diff } + set callback [lindex $cont_info 1] + if {$callback ne {}} { + eval $callback + } } } @@ -341,8 +460,9 @@ proc apply_hunk {x y} { } if {[catch { + set enc [get_path_encoding $current_diff_path] set p [eval git_write $apply_cmd] - fconfigure $p -translation binary -encoding binary + fconfigure $p -translation binary -encoding $enc puts -nonewline $p $current_diff_header puts -nonewline $p [$ui_diff get $s_lno $e_lno] close $p} err]} { @@ -370,10 +490,9 @@ proc apply_hunk {x y} { } unlock_index display_file $current_diff_path $mi + # This should trigger shift to the next changed file if {$o eq {_}} { - clear_diff - } else { - set current_diff_path $current_diff_path + reshow_diff } } @@ -511,8 +630,9 @@ proc apply_line {x y} { set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch" if {[catch { + set enc [get_path_encoding $current_diff_path] set p [eval git_write $apply_cmd] - fconfigure $p -translation binary -encoding binary + fconfigure $p -translation binary -encoding $enc puts -nonewline $p $current_diff_header puts -nonewline $p $patch close $p} err]} { diff --git a/git-gui/lib/encoding.tcl b/git-gui/lib/encoding.tcl index 7f06b0d47f..32668fc9c6 100644 --- a/git-gui/lib/encoding.tcl +++ b/git-gui/lib/encoding.tcl @@ -206,7 +206,7 @@ set encoding_aliases { { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 } { GBK CP936 MS936 windows-936 } { JIS_Encoding csJISEncoding } - { Shift_JIS MS_Kanji csShiftJIS } + { Shift_JIS MS_Kanji csShiftJIS ShiftJIS Shift-JIS } { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese EUC-JP } { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese } @@ -240,37 +240,227 @@ set encoding_aliases { { Big5 csBig5 } } +set encoding_groups { + {"" "" + {"Unicode" UTF-8} + {"Western" ISO-8859-1}} + {we "West European" + {"Western" ISO-8859-15 CP-437 CP-850 MacRoman CP-1252 Windows-1252} + {"Celtic" ISO-8859-14} + {"Greek" ISO-8859-14 ISO-8859-7 CP-737 CP-869 MacGreek CP-1253 Windows-1253} + {"Icelandic" MacIceland MacIcelandic CP-861} + {"Nordic" ISO-8859-10 CP-865} + {"Portuguese" CP-860} + {"South European" ISO-8859-3}} + {ee "East European" + {"Baltic" CP-775 ISO-8859-4 ISO-8859-13 CP-1257 Windows-1257} + {"Central European" CP-852 ISO-8859-2 MacCE CP-1250 Windows-1250} + {"Croatian" MacCroatian} + {"Cyrillic" CP-855 ISO-8859-5 ISO-IR-111 KOI8-R MacCyrillic CP-1251 Windows-1251} + {"Russian" CP-866} + {"Ukrainian" KOI8-U MacUkraine MacUkrainian} + {"Romanian" ISO-8859-16 MacRomania MacRomanian}} + {ea "East Asian" + {"Generic" ISO-2022} + {"Chinese Simplified" GB2312 GB1988 GB12345 GB2312-RAW GBK EUC-CN GB18030 HZ ISO-2022-CN} + {"Chinese Traditional" Big5 Big5-HKSCS EUC-TW CP-950} + {"Japanese" EUC-JP ISO-2022-JP Shift-JIS JIS-0212 JIS-0208 JIS-0201 CP-932 MacJapan} + {"Korean" EUC-KR UHC JOHAB ISO-2022-KR CP-949 KSC5601}} + {sa "SE & SW Asian" + {"Armenian" ARMSCII-8} + {"Georgian" GEOSTD8} + {"Thai" TIS-620 ISO-8859-11 CP-874 Windows-874 MacThai} + {"Turkish" CP-857 CP857 ISO-8859-9 MacTurkish CP-1254 Windows-1254} + {"Vietnamese" TCVN VISCII VPS CP-1258 Windows-1258} + {"Hindi" MacDevanagari} + {"Gujarati" MacGujarati} + {"Gurmukhi" MacGurmukhi}} + {me "Middle Eastern" + {"Arabic" ISO-8859-6 Windows-1256 CP-1256 CP-864 MacArabic} + {"Farsi" MacFarsi} + {"Hebrew" ISO-8859-8-I Windows-1255 CP-1255 ISO-8859-8 CP-862 MacHebrew}} + {mi "Misc" + {"7-bit" ASCII} + {"16-bit" Unicode} + {"Legacy" CP-863 EBCDIC} + {"Symbol" Symbol Dingbats MacDingbats MacCentEuro}} +} + +proc build_encoding_table {} { + global encoding_aliases encoding_lookup_table + + # Prepare the lookup list; cannot use lsort -nocase because + # of compatibility issues with older Tcl (e.g. in msysgit) + set names [list] + foreach item [encoding names] { + lappend names [list [string tolower $item] $item] + } + set names [lsort -ascii -index 0 $names] + # neither can we use lsearch -index + set lnames [list] + foreach item $names { + lappend lnames [lindex $item 0] + } + + foreach grp $encoding_aliases { + set target {} + foreach item $grp { + set i [lsearch -sorted -ascii $lnames \ + [string tolower $item]] + if {$i >= 0} { + set target [lindex $names $i 1] + break + } + } + if {$target eq {}} continue + foreach item $grp { + set encoding_lookup_table([string tolower $item]) $target + } + } + + foreach item $names { + set encoding_lookup_table([lindex $item 0]) [lindex $item 1] + } +} + proc tcl_encoding {enc} { - global encoding_aliases - set names [encoding names] - set lcnames [string tolower $names] - set enc [string tolower $enc] - set i [lsearch -exact $lcnames $enc] - if {$i < 0} { - # look for "isonnn" instead of "iso-nnn" or "iso_nnn" - if {[regsub {^iso[-_]} $enc iso encx]} { - set i [lsearch -exact $lcnames $encx] + global encoding_lookup_table + if {$enc eq {}} { + return {} + } + if {![info exists encoding_lookup_table]} { + build_encoding_table + } + set enc [string tolower $enc] + if {![info exists encoding_lookup_table($enc)]} { + # look for "isonnn" instead of "iso-nnn" or "iso_nnn" + if {[regsub {^(iso|cp|ibm|jis)[-_]} $enc {\1} encx]} { + set enc $encx + } + } + if {[info exists encoding_lookup_table($enc)]} { + return $encoding_lookup_table($enc) + } else { + return {} + } +} + +proc force_path_encoding {path enc} { + global path_encoding_overrides last_encoding_override + + set enc [tcl_encoding $enc] + if {$enc eq {}} { + catch { unset last_encoding_override } + catch { unset path_encoding_overrides($path) } + } else { + set last_encoding_override $enc + if {$path ne {}} { + set path_encoding_overrides($path) $enc + } + } +} + +proc get_path_encoding {path} { + global path_encoding_overrides last_encoding_override + + if {[info exists last_encoding_override]} { + set tcl_enc $last_encoding_override + } else { + set tcl_enc [tcl_encoding [get_config gui.encoding]] } - } - if {$i < 0} { - foreach l $encoding_aliases { - set ll [string tolower $l] - if {[lsearch -exact $ll $enc] < 0} continue - # look through the aliases for one that tcl knows about - foreach e $ll { - set i [lsearch -exact $lcnames $e] - if {$i < 0} { - if {[regsub {^iso[-_]} $e iso ex]} { - set i [lsearch -exact $lcnames $ex] - } + if {$tcl_enc eq {}} { + set tcl_enc [encoding system] + } + if {$path ne {}} { + if {[info exists path_encoding_overrides($path)]} { + set enc2 $path_encoding_overrides($path) + } else { + set enc2 [tcl_encoding [gitattr $path encoding $tcl_enc]] + } + if {$enc2 ne {}} { + set tcl_enc $enc2 + } + } + return $tcl_enc +} + +proc build_encoding_submenu {parent grp cmd} { + global used_encodings + + set mid [lindex $grp 0] + set gname [mc [lindex $grp 1]] + + set smenu {} + foreach subset [lrange $grp 2 end] { + set name [mc [lindex $subset 0]] + + foreach enc [lrange $subset 1 end] { + set tcl_enc [tcl_encoding $enc] + if {$tcl_enc eq {}} continue + + if {$smenu eq {}} { + if {$mid eq {}} { + set smenu $parent + } else { + set smenu "$parent.$mid" + menu $smenu + $parent add cascade \ + -label $gname \ + -menu $smenu + } + } + + if {$name ne {}} { + set lbl "$name ($enc)" + } else { + set lbl $enc + } + $smenu add command \ + -label $lbl \ + -command [concat $cmd [list $tcl_enc]] + + lappend used_encodings $tcl_enc + } + } +} + +proc popup_btn_menu {m b} { + tk_popup $m [winfo pointerx $b] [winfo pointery $b] +} + +proc build_encoding_menu {emenu cmd {nodef 0}} { + $emenu configure -postcommand \ + [list do_build_encoding_menu $emenu $cmd $nodef] +} + +proc do_build_encoding_menu {emenu cmd {nodef 0}} { + global used_encodings encoding_groups + + $emenu configure -postcommand {} + + if {!$nodef} { + $emenu add command \ + -label [mc "Default"] \ + -command [concat $cmd [list {}]] + } + set sysenc [encoding system] + $emenu add command \ + -label [mc "System (%s)" $sysenc] \ + -command [concat $cmd [list $sysenc]] + + # Main encoding tree + set used_encodings [list identity] + $emenu add separator + foreach grp $encoding_groups { + build_encoding_submenu $emenu $grp $cmd + } + + # Add unclassified encodings + set unused_grp [list [mc Other]] + foreach enc [encoding names] { + if {[lsearch -exact $used_encodings $enc] < 0} { + lappend unused_grp $enc } - if {$i >= 0} break - } - break } - } - if {$i >= 0} { - return [lindex $names $i] - } - return {} + build_encoding_submenu $emenu [list other [mc Other] $unused_grp] $cmd } diff --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl index 3c1fce7475..d33896a0ce 100644 --- a/git-gui/lib/index.tcl +++ b/git-gui/lib/index.tcl @@ -99,6 +99,7 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} { switch -glob -- [lindex $s 0] { A? {set new _O} M? {set new _M} + T_ {set new _T} D_ {set new _D} D? {set new _?} ?? {continue} @@ -162,6 +163,8 @@ proc write_update_index {fd pathList totalCnt batch after} { ?D {set new D_} _O - AM {set new A_} + _T {set new T_} + _U - U? { if {[file exists $path]} { set new M_ @@ -231,6 +234,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} { switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - + ?T - ?D { puts -nonewline $fd "[encoding convertto $path]\0" display_file $path ?_ @@ -252,6 +256,7 @@ proc unstage_helper {txt paths} { switch -glob -- [lindex $file_states($path) 0] { A? - M? - + T_ - D? { lappend pathList $path if {$path eq $current_diff_path} { @@ -293,10 +298,18 @@ proc add_helper {txt paths} { 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 - - U? { + ?T { lappend pathList $path if {$path eq $current_diff_path} { set after {reshow_diff;} @@ -336,6 +349,7 @@ proc do_add_all {} { switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - + ?T - ?D {lappend paths $path} } } @@ -353,6 +367,7 @@ proc revert_helper {txt paths} { switch -glob -- [lindex $file_states($path) 0] { U? {continue} ?M - + ?T - ?D { lappend pathList $path if {$path eq $current_diff_path} { @@ -409,11 +424,11 @@ proc do_revert_selection {} { if {[array size selected_paths] > 0} { revert_helper \ - {Reverting selected files} \ + [mc "Reverting selected files"] \ [array names selected_paths] } elseif {$current_diff_path ne {}} { revert_helper \ - "Reverting [short_path $current_diff_path]" \ + [mc "Reverting %s" [short_path $current_diff_path]] \ [list $current_diff_path] } } diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl index 5c01875b05..283e4915e9 100644 --- a/git-gui/lib/merge.tcl +++ b/git-gui/lib/merge.tcl @@ -40,6 +40,7 @@ The rescan will be automatically started now. _O { continue; # and pray it works! } + _U - U? { error_popup [mc "You are in the middle of a conflicted merge. diff --git a/git-gui/lib/mergetool.tcl b/git-gui/lib/mergetool.tcl new file mode 100644 index 0000000000..eb2b4b56a4 --- /dev/null +++ b/git-gui/lib/mergetool.tcl @@ -0,0 +1,393 @@ +# git-gui merge conflict resolution +# parts based on git-mergetool (c) 2006 Theodore Y. Ts'o + +proc merge_resolve_one {stage} { + global current_diff_path + + switch -- $stage { + 1 { set targetquestion [mc "Force resolution to the base version?"] } + 2 { set targetquestion [mc "Force resolution to this branch?"] } + 3 { set targetquestion [mc "Force resolution to the other branch?"] } + } + + set op_question [strcat $targetquestion "\n" \ +[mc "Note that the diff shows only conflicting changes. + +%s will be overwritten. + +This operation can be undone only by restarting the merge." \ + [short_path $current_diff_path]]] + + if {[ask_popup $op_question] eq {yes}} { + merge_load_stages $current_diff_path [list merge_force_stage $stage] + } +} + +proc merge_stage_workdir {path {lno {}}} { + global current_diff_path diff_active + global current_diff_side ui_workdir + + if {$diff_active} return + + if {$path ne $current_diff_path || $ui_workdir ne $current_diff_side} { + show_diff $path $ui_workdir $lno {} [list do_merge_stage_workdir $path] + } else { + do_merge_stage_workdir $path + } +} + +proc do_merge_stage_workdir {path} { + global current_diff_path is_conflict_diff + + if {$path ne $current_diff_path} return; + + if {$is_conflict_diff} { + if {[ask_popup [mc "File %s seems to have unresolved conflicts, still stage?" \ + [short_path $path]]] ne {yes}} { + return + } + } + + merge_add_resolution $path +} + +proc merge_add_resolution {path} { + global current_diff_path ui_workdir + + set after [next_diff_after_action $ui_workdir $path {} {^_?U}] + + update_index \ + [mc "Adding resolution for %s" [short_path $path]] \ + [list $path] \ + [concat $after [list ui_ready]] +} + +proc merge_force_stage {stage} { + global current_diff_path merge_stages + + if {$merge_stages($stage) ne {}} { + git checkout-index -f --stage=$stage -- $current_diff_path + } else { + file delete -- $current_diff_path + } + + merge_add_resolution $current_diff_path +} + +proc merge_load_stages {path cont} { + global merge_stages_fd merge_stages merge_stages_buf + + if {[info exists merge_stages_fd]} { + catch { kill_file_process $merge_stages_fd } + catch { close $merge_stages_fd } + } + + set merge_stages(0) {} + set merge_stages(1) {} + set merge_stages(2) {} + set merge_stages(3) {} + set merge_stages_buf {} + + set merge_stages_fd [eval git_read ls-files -u -z -- $path] + + fconfigure $merge_stages_fd -blocking 0 -translation binary -encoding binary + fileevent $merge_stages_fd readable [list read_merge_stages $merge_stages_fd $cont] +} + +proc read_merge_stages {fd cont} { + global merge_stages_buf merge_stages_fd merge_stages + + append merge_stages_buf [read $fd] + set pck [split $merge_stages_buf "\0"] + set merge_stages_buf [lindex $pck end] + + if {[eof $fd] && $merge_stages_buf ne {}} { + lappend pck {} + set merge_stages_buf {} + } + + foreach p [lrange $pck 0 end-1] { + set fcols [split $p "\t"] + set cols [split [lindex $fcols 0] " "] + set stage [lindex $cols 2] + + set merge_stages($stage) [lrange $cols 0 1] + } + + if {[eof $fd]} { + close $fd + unset merge_stages_fd + eval $cont + } +} + +proc merge_resolve_tool {} { + global current_diff_path + + merge_load_stages $current_diff_path [list merge_resolve_tool2] +} + +proc merge_resolve_tool2 {} { + global current_diff_path merge_stages + + # Validate the stages + if {$merge_stages(2) eq {} || + [lindex $merge_stages(2) 0] eq {120000} || + [lindex $merge_stages(2) 0] eq {160000} || + $merge_stages(3) eq {} || + [lindex $merge_stages(3) 0] eq {120000} || + [lindex $merge_stages(3) 0] eq {160000} + } { + error_popup [mc "Cannot resolve deletion or link conflicts using a tool"] + return + } + + if {![file exists $current_diff_path]} { + error_popup [mc "Conflict file does not exist"] + return + } + + # Determine the tool to use + set tool [get_config merge.tool] + if {$tool eq {}} { set tool meld } + + set merge_tool_path [get_config "mergetool.$tool.path"] + if {$merge_tool_path eq {}} { + switch -- $tool { + emerge { set merge_tool_path "emacs" } + araxis { set merge_tool_path "compare" } + default { set merge_tool_path $tool } + } + } + + # Make file names + set filebase [file rootname $current_diff_path] + set fileext [file extension $current_diff_path] + set basename [lindex [file split $current_diff_path] end] + + set MERGED $current_diff_path + set BASE "./$MERGED.BASE$fileext" + set LOCAL "./$MERGED.LOCAL$fileext" + set REMOTE "./$MERGED.REMOTE$fileext" + set BACKUP "./$MERGED.BACKUP$fileext" + + set base_stage $merge_stages(1) + + # Build the command line + switch -- $tool { + kdiff3 { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Base)" \ + --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Local)" \ + --L2 "$MERGED (Remote)" -o "$MERGED" "$LOCAL" "$REMOTE"] + } + } + tkdiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"] + } + } + meld { + set cmdline [list "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"] + } + gvimdiff { + set cmdline [list "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"] + } + xxdiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -X --show-merged-pane \ + -R {Accel.SaveAsMerged: "Ctrl-S"} \ + -R {Accel.Search: "Ctrl+F"} \ + -R {Accel.SearchForward: "Ctrl-G"} \ + --merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" -X --show-merged-pane \ + -R {Accel.SaveAsMerged: "Ctrl-S"} \ + -R {Accel.Search: "Ctrl+F"} \ + -R {Accel.SearchForward: "Ctrl-G"} \ + --merged-file "$MERGED" "$LOCAL" "$REMOTE"] + } + } + opendiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED"] + } + } + ecmerge { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"] + } else { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"] + } + } + emerge { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -f emerge-files-with-ancestor-command \ + "$LOCAL" "$REMOTE" "$BASE" "$basename"] + } else { + set cmdline [list "$merge_tool_path" -f emerge-files-command \ + "$LOCAL" "$REMOTE" "$basename"] + } + } + winmerge { + if {$base_stage ne {}} { + # This tool does not support 3-way merges. + # Use the 'conflict file' resolution feature instead. + set cmdline [list "$merge_tool_path" -e -ub "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" -e -ub -wl \ + -dl "Theirs File" -dr "Mine File" "$REMOTE" "$LOCAL" "$MERGED"] + } + } + araxis { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -wait -merge -3 -a1 \ + -title1:"'$MERGED (Base)'" -title2:"'$MERGED (Local)'" \ + -title3:"'$MERGED (Remote)'" \ + "$BASE" "$LOCAL" "$REMOTE" "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" -wait -2 \ + -title1:"'$MERGED (Local)'" -title2:"'$MERGED (Remote)'" \ + "$LOCAL" "$REMOTE" "$MERGED"] + } + } + p4merge { + set cmdline [list "$merge_tool_path" "$BASE" "$REMOTE" "$LOCAL" "$MERGED"] + } + vimdiff { + error_popup [mc "Not a GUI merge tool: '%s'" $tool] + return + } + default { + error_popup [mc "Unsupported merge tool '%s'" $tool] + return + } + } + + merge_tool_start $cmdline $MERGED $BACKUP [list $BASE $LOCAL $REMOTE] +} + +proc delete_temp_files {files} { + foreach fname $files { + file delete $fname + } +} + +proc merge_tool_get_stages {target stages} { + global merge_stages + + set i 1 + foreach fname $stages { + if {$merge_stages($i) eq {}} { + file delete $fname + catch { close [open $fname w] } + } else { + # A hack to support autocrlf properly + git checkout-index -f --stage=$i -- $target + file rename -force -- $target $fname + } + incr i + } +} + +proc merge_tool_start {cmdline target backup stages} { + global merge_stages mtool_target mtool_tmpfiles mtool_fd mtool_mtime + + if {[info exists mtool_fd]} { + if {[ask_popup [mc "Merge tool is already running, terminate it?"]] eq {yes}} { + catch { kill_file_process $mtool_fd } + catch { close $mtool_fd } + unset mtool_fd + + set old_backup [lindex $mtool_tmpfiles end] + file rename -force -- $old_backup $mtool_target + delete_temp_files $mtool_tmpfiles + } else { + return + } + } + + # Save the original file + file rename -force -- $target $backup + + # Get the blobs; it destroys $target + if {[catch {merge_tool_get_stages $target $stages} err]} { + file rename -force -- $backup $target + delete_temp_files $stages + error_popup [mc "Error retrieving versions:\n%s" $err] + return + } + + # Restore the conflict file + file copy -force -- $backup $target + + # Initialize global state + set mtool_target $target + set mtool_mtime [file mtime $target] + set mtool_tmpfiles $stages + + lappend mtool_tmpfiles $backup + + # Force redirection to avoid interpreting output on stderr + # as an error, and launch the tool + lappend cmdline {2>@1} + + if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} { + delete_temp_files $mtool_tmpfiles + error_popup [mc "Could not start the merge tool:\n\n%s" $err] + return + } + + ui_status [mc "Running merge tool..."] + + fconfigure $mtool_fd -blocking 0 -translation binary -encoding binary + fileevent $mtool_fd readable [list read_mtool_output $mtool_fd] +} + +proc read_mtool_output {fd} { + global mtool_fd mtool_tmpfiles + + read $fd + if {[eof $fd]} { + unset mtool_fd + + fconfigure $fd -blocking 1 + merge_tool_finish $fd + } +} + +proc merge_tool_finish {fd} { + global mtool_tmpfiles mtool_target mtool_mtime + + set backup [lindex $mtool_tmpfiles end] + set failed 0 + + # Check the return code + if {[catch {close $fd} err]} { + set failed 1 + if {$err ne {child process exited abnormally}} { + error_popup [strcat [mc "Merge tool failed."] "\n\n$err"] + } + } + + # Finish + if {$failed} { + file rename -force -- $backup $mtool_target + delete_temp_files $mtool_tmpfiles + ui_status [mc "Merge tool failed."] + } else { + if {[is_config_true merge.keepbackup]} { + file rename -force -- $backup "$mtool_target.orig" + } + + delete_temp_files $mtool_tmpfiles + + reshow_diff + } +} diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl index 5e1346e601..1d55b49c9b 100644 --- a/git-gui/lib/option.tcl +++ b/git-gui/lib/option.tcl @@ -1,9 +1,31 @@ # git-gui options editor # Copyright (C) 2006, 2007 Shawn Pearce +proc config_check_encodings {} { + global repo_config_new global_config_new + + set enc $global_config_new(gui.encoding) + if {$enc eq {}} { + set global_config_new(gui.encoding) [encoding system] + } elseif {[tcl_encoding $enc] eq {}} { + error_popup [mc "Invalid global encoding '%s'" $enc] + return 0 + } + + set enc $repo_config_new(gui.encoding) + if {$enc eq {}} { + set repo_config_new(gui.encoding) [encoding system] + } elseif {[tcl_encoding $enc] eq {}} { + error_popup [mc "Invalid repo encoding '%s'" $enc] + return 0 + } + + return 1 +} + proc save_config {} { global default_config font_descs - global repo_config global_config + global repo_config global_config system_config global repo_config_new global_config_new global ui_comm_spell @@ -27,7 +49,7 @@ proc save_config {} { foreach name [array names default_config] { set value $global_config_new($name) if {$value ne $global_config($name)} { - if {$value eq $default_config($name)} { + if {$value eq $system_config($name)} { catch {git config --global --unset $name} } else { regsub -all "\[{}\]" $value {"} value @@ -119,15 +141,18 @@ proc do_options {} { {b merge.summary {mc "Summarize Merge Commits"}} {i-1..5 merge.verbosity {mc "Merge Verbosity"}} {b merge.diffstat {mc "Show Diffstat After Merge"}} + {t merge.tool {mc "Use Merge Tool"}} {b gui.trustmtime {mc "Trust File Modification Timestamps"}} {b gui.pruneduringfetch {mc "Prune Tracking Branches During Fetch"}} {b gui.matchtrackingbranch {mc "Match Tracking Branches"}} {b gui.fastcopyblame {mc "Blame Copy Only On Changed Files"}} {i-20..200 gui.copyblamethreshold {mc "Minimum Letters To Blame Copy On"}} + {i-0..300 gui.blamehistoryctx {mc "Blame History Context Radius (days)"}} {i-1..99 gui.diffcontext {mc "Number of Diff Context Lines"}} {i-0..99 gui.commitmsgwidth {mc "Commit Message Text Width"}} {t gui.newbranchtemplate {mc "New Branch Name Template"}} + {c gui.encoding {mc "Default File Contents Encoding"}} } { set type [lindex $option 0] set name [lindex $option 1] @@ -157,6 +182,7 @@ proc do_options {} { pack $w.$f.$optid.v -side right -anchor e -padx 5 pack $w.$f.$optid -side top -anchor w -fill x } + c - t { frame $w.$f.$optid label $w.$f.$optid.l -text "$text:" @@ -169,6 +195,16 @@ proc do_options {} { pack $w.$f.$optid.v -side left -anchor w \ -fill x -expand 1 \ -padx 5 + if {$type eq {c}} { + menu $w.$f.$optid.m + build_encoding_menu $w.$f.$optid.m \ + [list set ${f}_config_new($name)] 1 + button $w.$f.$optid.b \ + -text [mc "Change"] \ + -command [list popup_btn_menu \ + $w.$f.$optid.m $w.$f.$optid.b] + pack $w.$f.$optid.b -side left -anchor w + } pack $w.$f.$optid -side top -anchor w -fill x } } @@ -248,17 +284,17 @@ proc do_options {} { } proc do_restore_defaults {} { - global font_descs default_config repo_config + global font_descs default_config repo_config system_config global repo_config_new global_config_new foreach name [array names default_config] { - set repo_config_new($name) $default_config($name) - set global_config_new($name) $default_config($name) + set repo_config_new($name) $system_config($name) + set global_config_new($name) $system_config($name) } foreach option $font_descs { set name [lindex $option 0] - set repo_config(gui.$name) $default_config(gui.$name) + set repo_config(gui.$name) $system_config(gui.$name) } apply_config @@ -273,6 +309,7 @@ proc do_restore_defaults {} { } proc do_save_config {w} { + if {![config_check_encodings]} return if {[catch {save_config} err]} { error_popup [strcat [mc "Failed to completely save options:"] "\n\n$err"] } diff --git a/git-gui/lib/remote.tcl b/git-gui/lib/remote.tcl index 0e86ddac09..b92b429cf7 100644 --- a/git-gui/lib/remote.tcl +++ b/git-gui/lib/remote.tcl @@ -132,91 +132,145 @@ proc load_all_remotes {} { set all_remotes [lsort -unique $all_remotes] } -proc populate_fetch_menu {} { - global all_remotes repo_config - +proc add_fetch_entry {r} { + global repo_config set remote_m .mbar.remote set fetch_m $remote_m.fetch set prune_m $remote_m.prune - - foreach r $all_remotes { - set enable 0 - if {![catch {set a $repo_config(remote.$r.url)}]} { - if {![catch {set a $repo_config(remote.$r.fetch)}]} { - set enable 1 - } - } else { - catch { - set fd [open [gitdir remotes $r] r] - while {[gets $fd n] >= 0} { - if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { - set enable 1 - break - } + set remove_m $remote_m.remove + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.fetch)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { + set enable 1 + break } - close $fd } + close $fd } + } - if {$enable} { - if {![winfo exists $fetch_m]} { - menu $prune_m - $remote_m insert 0 cascade \ - -label [mc "Prune from"] \ - -menu $prune_m - - menu $fetch_m - $remote_m insert 0 cascade \ - -label [mc "Fetch from"] \ - -menu $fetch_m - } + if {$enable} { + if {![winfo exists $fetch_m]} { + menu $remove_m + $remote_m insert 0 cascade \ + -label [mc "Remove Remote"] \ + -menu $remove_m + + menu $prune_m + $remote_m insert 0 cascade \ + -label [mc "Prune from"] \ + -menu $prune_m - $fetch_m add command \ - -label $r \ - -command [list fetch_from $r] - $prune_m add command \ - -label $r \ - -command [list prune_from $r] + menu $fetch_m + $remote_m insert 0 cascade \ + -label [mc "Fetch from"] \ + -menu $fetch_m } + + $fetch_m add command \ + -label $r \ + -command [list fetch_from $r] + $prune_m add command \ + -label $r \ + -command [list prune_from $r] + $remove_m add command \ + -label $r \ + -command [list remove_remote $r] } } -proc populate_push_menu {} { - global all_remotes repo_config - +proc add_push_entry {r} { + global repo_config set remote_m .mbar.remote set push_m $remote_m.push - - foreach r $all_remotes { - set enable 0 - if {![catch {set a $repo_config(remote.$r.url)}]} { - if {![catch {set a $repo_config(remote.$r.push)}]} { - set enable 1 - } - } else { - catch { - set fd [open [gitdir remotes $r] r] - while {[gets $fd n] >= 0} { - if {[regexp {^Push:[ \t]*([^:]+):} $n]} { - set enable 1 - break - } + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.push)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Push:[ \t]*([^:]+):} $n]} { + set enable 1 + break } - close $fd } + close $fd } + } - if {$enable} { - if {![winfo exists $push_m]} { - menu $push_m - $remote_m insert 0 cascade \ - -label [mc "Push to"] \ - -menu $push_m - } - - $push_m add command \ - -label $r \ - -command [list push_to $r] + if {$enable} { + if {![winfo exists $push_m]} { + menu $push_m + $remote_m insert 0 cascade \ + -label [mc "Push to"] \ + -menu $push_m } + + $push_m add command \ + -label $r \ + -command [list push_to $r] + } +} + +proc populate_remotes_menu {} { + global all_remotes + + foreach r $all_remotes { + add_fetch_entry $r + add_push_entry $r + } +} + +proc add_single_remote {name location} { + global all_remotes repo_config + lappend all_remotes $name + + git remote add $name $location + + # XXX: Better re-read the config so that we will never get out + # of sync with git remote implementation? + set repo_config(remote.$name.url) $location + set repo_config(remote.$name.fetch) "+refs/heads/*:refs/remotes/$name/*" + + add_fetch_entry $name + add_push_entry $name +} + +proc delete_from_menu {menu name} { + if {[winfo exists $menu]} { + $menu delete $name } } + +proc remove_remote {name} { + global all_remotes repo_config + + git remote rm $name + + catch { + # Missing values are ok + unset repo_config(remote.$name.url) + unset repo_config(remote.$name.fetch) + unset repo_config(remote.$name.push) + } + + set i [lsearch -exact all_remotes $name] + lreplace all_remotes $i $i + + set remote_m .mbar.remote + delete_from_menu $remote_m.fetch $name + delete_from_menu $remote_m.prune $name + delete_from_menu $remote_m.remove $name + # Not all remotes are in the push menu + catch { delete_from_menu $remote_m.push $name } +} diff --git a/git-gui/lib/remote_add.tcl b/git-gui/lib/remote_add.tcl new file mode 100644 index 0000000000..fb29422aa7 --- /dev/null +++ b/git-gui/lib/remote_add.tcl @@ -0,0 +1,191 @@ +# git-gui remote adding support +# Copyright (C) 2008 Petr Baudis + +class remote_add { + +field w ; # widget path +field w_name ; # new remote name widget +field w_loc ; # new remote location widget + +field name {}; # name of the remote the user has chosen +field location {}; # location of the remote the user has chosen + +field opt_action fetch; # action to do after registering the remote locally + +constructor dialog {} { + global repo_config + + make_toplevel top w + wm title $top [append "[appname] ([reponame]): " [mc "Add Remote"]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + label $w.header -text [mc "Add New Remote"] -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + button $w.buttons.create -text [mc Add] \ + -default active \ + -command [cb _add] + pack $w.buttons.create -side right + button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + labelframe $w.desc -text [mc "Remote Details"] + + label $w.desc.name_l -text [mc "Name:"] + set w_name $w.desc.name_t + entry $w_name \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @name \ + -validate key \ + -validatecommand [cb _validate_name %d %S] + grid $w.desc.name_l $w_name -sticky we -padx {0 5} + + label $w.desc.loc_l -text [mc "Location:"] + set w_loc $w.desc.loc_t + entry $w_loc \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @location + grid $w.desc.loc_l $w_loc -sticky we -padx {0 5} + + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + + labelframe $w.action -text [mc "Further Action"] + + radiobutton $w.action.fetch \ + -text [mc "Fetch Immediately"] \ + -value fetch \ + -variable @opt_action + pack $w.action.fetch -anchor nw + + radiobutton $w.action.push \ + -text [mc "Initialize Remote Repository and Push"] \ + -value push \ + -variable @opt_action + pack $w.action.push -anchor nw + + radiobutton $w.action.none \ + -text [mc "Do Nothing Else Now"] \ + -value none \ + -variable @opt_action + pack $w.action.none -anchor nw + + grid columnconfigure $w.action 1 -weight 1 + pack $w.action -anchor nw -fill x -pady 5 -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _add]\;break + tkwait window $w +} + +method _add {} { + global repo_config env + global M1B + + if {$name eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please supply a remote name."] + focus $w_name + return + } + + # XXX: We abuse check-ref-format here, but + # that should be ok. + if {[catch {git check-ref-format "remotes/$name"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "'%s' is not an acceptable remote name." $name] + focus $w_name + return + } + + if {[catch {add_single_remote $name $location}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Failed to add remote '%s' of location '%s'." $name $location] + focus $w_name + return + } + + switch -- $opt_action { + fetch { + set c [console::new \ + [mc "fetch %s" $name] \ + [mc "Fetching the %s" $name]] + console::exec $c [list git fetch $name] + } + push { + set cmds [list] + + # Parse the location + if { [regexp {(?:git\+)?ssh://([^/]+)(/.+)} $location xx host path] + || [regexp {([^:][^:]+):(.+)} $location xx host path]} { + set ssh ssh + if {[info exists env(GIT_SSH)]} { + set ssh $env(GIT_SSH) + } + lappend cmds [list exec $ssh $host mkdir -p $location && git --git-dir=$path init --bare] + } elseif { ! [regexp {://} $location xx] } { + lappend cmds [list exec mkdir -p $location] + lappend cmds [list exec git --git-dir=$location init --bare] + } else { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Do not know how to initialize repository at location '%s'." $location] + destroy $w + return + } + + set c [console::new \ + [mc "push %s" $name] \ + [mc "Setting up the %s (at %s)" $name $location]] + + lappend cmds [list exec git push -v --all $name] + console::chain $c $cmds + } + none { + } + } + + destroy $w +} + +method _validate_name {d S} { + if {$d == 1} { + if {[regexp {[~^:?*\[\0- ]} $S]} { + return 0 + } + } + return 1 +} + +method _visible {} { + grab $w + $w_name icursor end + focus $w_name +} + +} diff --git a/git-gui/lib/remote_branch_delete.tcl b/git-gui/lib/remote_branch_delete.tcl index c7b8148698..89eb0f70f2 100644 --- a/git-gui/lib/remote_branch_delete.tcl +++ b/git-gui/lib/remote_branch_delete.tcl @@ -26,12 +26,12 @@ constructor dialog {} { global all_remotes M1B make_toplevel top w - wm title $top [append "[appname] ([reponame]): " [mc "Delete Remote Branch"]] + wm title $top [append "[appname] ([reponame]): " [mc "Delete Branch Remotely"]] if {$top ne {.}} { wm geometry $top "+[winfo rootx .]+[winfo rooty .]" } - label $w.header -text [mc "Delete Remote Branch"] -font font_uibold + label $w.header -text [mc "Delete Branch Remotely"] -font font_uibold pack $w.header -side top -fill x frame $w.buttons @@ -63,7 +63,7 @@ constructor dialog {} { set urltype url } radiobutton $w.dest.url_r \ - -text [mc "Arbitrary URL:"] \ + -text [mc "Arbitrary Location:"] \ -value url \ -variable @urltype entry $w.dest.url_t \ diff --git a/git-gui/lib/search.tcl b/git-gui/lib/search.tcl new file mode 100644 index 0000000000..b371e9a30a --- /dev/null +++ b/git-gui/lib/search.tcl @@ -0,0 +1,198 @@ +# incremental search panel +# based on code from gitk, Copyright (C) Paul Mackerras + +class searchbar { + +field w +field ctext + +field searchstring {} +field casesensitive 1 +field searchdirn -forwards + +field smarktop +field smarkbot + +constructor new {i_w i_text args} { + set w $i_w + set ctext $i_text + + frame $w + label $w.l -text [mc Find:] + entry $w.ent -textvariable ${__this}::searchstring -background lightgreen + button $w.bn -text [mc Next] -command [cb find_next] + button $w.bp -text [mc Prev] -command [cb find_prev] + checkbutton $w.cs -text [mc Case-Sensitive] \ + -variable ${__this}::casesensitive -command [cb _incrsearch] + pack $w.l -side left + pack $w.cs -side right + pack $w.bp -side right + pack $w.bn -side right + pack $w.ent -side left -expand 1 -fill x + + eval grid conf $w -sticky we $args + grid remove $w + + trace add variable searchstring write [cb _incrsearch_cb] + + bind $w <Destroy> [list delete_this $this] + return $this +} + +method show {} { + if {![visible $this]} { + grid $w + } + focus -force $w.ent +} + +method hide {} { + if {[visible $this]} { + focus $ctext + grid remove $w + } +} + +method visible {} { + return [winfo ismapped $w] +} + +method editor {} { + return $w.ent +} + +method _get_new_anchor {} { + # use start of selection if it is visible, + # or the bounds of the visible area + set top [$ctext index @0,0] + set bottom [$ctext index @0,[winfo height $ctext]] + set sel [$ctext tag ranges sel] + if {$sel ne {}} { + set spos [lindex $sel 0] + if {[lindex $spos 0] >= [lindex $top 0] && + [lindex $spos 0] <= [lindex $bottom 0]} { + return $spos + } + } + if {$searchdirn eq "-forwards"} { + return $top + } else { + return $bottom + } +} + +method _get_wrap_anchor {dir} { + if {$dir eq "-forwards"} { + return 1.0 + } else { + return end + } +} + +method _do_search {start {mlenvar {}} {dir {}} {endbound {}}} { + set cmd [list $ctext search] + if {$mlenvar ne {}} { + upvar $mlenvar mlen + lappend cmd -count mlen + } + if {!$casesensitive} { + lappend cmd -nocase + } + if {$dir eq {}} { + set dir $searchdirn + } + lappend cmd $dir -- $searchstring + if {$endbound ne {}} { + set here [eval $cmd [list $start] [list $endbound]] + } else { + set here [eval $cmd [list $start]] + if {$here eq {}} { + set here [eval $cmd [_get_wrap_anchor $this $dir]] + } + } + return $here +} + +method _incrsearch_cb {name ix op} { + after idle [cb _incrsearch] +} + +method _incrsearch {} { + $ctext tag remove found 1.0 end + if {[catch {$ctext index anchor}]} { + $ctext mark set anchor [_get_new_anchor $this] + } + if {$searchstring ne {}} { + set here [_do_search $this anchor mlen] + if {$here ne {}} { + $ctext see $here + $ctext tag remove sel 1.0 end + $ctext tag add sel $here "$here + $mlen c" + $w.ent configure -background lightgreen + _set_marks $this 1 + } else { + $w.ent configure -background lightpink + } + } +} + +method find_prev {} { + find_next $this -backwards +} + +method find_next {{dir -forwards}} { + focus $w.ent + $w.ent icursor end + set searchdirn $dir + $ctext mark unset anchor + if {$searchstring ne {}} { + set start [_get_new_anchor $this] + if {$dir eq "-forwards"} { + set start "$start + 1c" + } + set match [_do_search $this $start mlen] + $ctext tag remove sel 1.0 end + if {$match ne {}} { + $ctext see $match + $ctext tag add sel $match "$match + $mlen c" + } + } +} + +method _mark_range {first last} { + set mend $first.0 + while {1} { + set match [_do_search $this $mend mlen -forwards $last.end] + if {$match eq {}} break + set mend "$match + $mlen c" + $ctext tag add found $match $mend + } +} + +method _set_marks {doall} { + set topline [lindex [split [$ctext index @0,0] .] 0] + set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0] + if {$doall || $botline < $smarktop || $topline > $smarkbot} { + # no overlap with previous + _mark_range $this $topline $botline + set smarktop $topline + set smarkbot $botline + } else { + if {$topline < $smarktop} { + _mark_range $this $topline [expr {$smarktop-1}] + set smarktop $topline + } + if {$botline > $smarkbot} { + _mark_range $this [expr {$smarkbot+1}] $botline + set smarkbot $botline + } + } +} + +method scrolled {} { + if {$searchstring ne {}} { + after idle [cb _set_marks 0] + } +} + +}
\ No newline at end of file diff --git a/git-gui/lib/spellcheck.tcl b/git-gui/lib/spellcheck.tcl index a479b2f285..e6120303e9 100644 --- a/git-gui/lib/spellcheck.tcl +++ b/git-gui/lib/spellcheck.tcl @@ -314,6 +314,7 @@ method _run {} { method _read {} { while {[gets $s_fd line] >= 0} { set lineno [lindex $s_pending 0 0] + set line [string trim $line] if {$s_clear} { $w_text tag remove misspelled "$lineno.0" "$lineno.end" diff --git a/git-gui/lib/sshkey.tcl b/git-gui/lib/sshkey.tcl new file mode 100644 index 0000000000..82a1a80ff4 --- /dev/null +++ b/git-gui/lib/sshkey.tcl @@ -0,0 +1,126 @@ +# git-gui about git-gui dialog +# Copyright (C) 2006, 2007 Shawn Pearce + +proc find_ssh_key {} { + foreach name {~/.ssh/id_dsa.pub ~/.ssh/id_rsa.pub ~/.ssh/identity.pub} { + if {[file exists $name]} { + set fh [open $name r] + set cont [read $fh] + close $fh + return [list $name $cont] + } + } + + return {} +} + +proc do_ssh_key {} { + global sshkey_title have_tk85 sshkey_fd + + set w .sshkey_dialog + if {[winfo exists $w]} { + raise $w + return + } + + toplevel $w + wm transient $w . + + set finfo [find_ssh_key] + if {$finfo eq {}} { + set sshkey_title [mc "No keys found."] + set gen_state normal + } else { + set sshkey_title [mc "Found a public key in: %s" [lindex $finfo 0]] + set gen_state disabled + } + + frame $w.header -relief flat + label $w.header.lbl -textvariable sshkey_title -anchor w + button $w.header.gen -text [mc "Generate Key"] \ + -command [list make_ssh_key $w] -state $gen_state + pack $w.header.lbl -side left -expand 1 -fill x + pack $w.header.gen -side right + pack $w.header -fill x -pady 5 -padx 5 + + text $w.contents -width 60 -height 10 -wrap char -relief sunken + pack $w.contents -fill both -expand 1 + if {$have_tk85} { + $w.contents configure -inactiveselectbackground darkblue + } + + frame $w.buttons + button $w.buttons.close -text [mc Close] \ + -default active -command [list destroy $w] + pack $w.buttons.close -side right + button $w.buttons.copy -text [mc "Copy To Clipboard"] \ + -command [list tk_textCopy $w.contents] + pack $w.buttons.copy -side left + pack $w.buttons -side bottom -fill x -pady 5 -padx 5 + + if {$finfo ne {}} { + $w.contents insert end [lindex $finfo 1] sel + } + $w.contents configure -state disabled + + bind $w <Visibility> "grab $w; focus $w.buttons.close" + bind $w <Key-Escape> "destroy $w" + bind $w <Key-Return> "destroy $w" + bind $w <Destroy> kill_sshkey + wm title $w [mc "Your OpenSSH Public Key"] + tk::PlaceWindow $w widget . + tkwait window $w +} + +proc make_ssh_key {w} { + global sshkey_title sshkey_output sshkey_fd + + set sshkey_title [mc "Generating..."] + $w.header.gen configure -state disabled + + set cmdline [list sh -c {echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}] + + if {[catch { set sshkey_fd [_open_stdout_stderr $cmdline] } err]} { + error_popup [mc "Could not start ssh-keygen:\n\n%s" $err] + return + } + + set sshkey_output {} + fconfigure $sshkey_fd -blocking 0 + fileevent $sshkey_fd readable [list read_sshkey_output $sshkey_fd $w] +} + +proc kill_sshkey {} { + global sshkey_fd + if {![info exists sshkey_fd]} return + catch { kill_file_process $sshkey_fd } + catch { close $sshkey_fd } +} + +proc read_sshkey_output {fd w} { + global sshkey_fd sshkey_output sshkey_title + + set sshkey_output "$sshkey_output[read $fd]" + if {![eof $fd]} return + + fconfigure $fd -blocking 1 + unset sshkey_fd + + $w.contents configure -state normal + if {[catch {close $fd} err]} { + set sshkey_title [mc "Generation failed."] + $w.contents insert end $err + $w.contents insert end "\n" + $w.contents insert end $sshkey_output + } else { + set finfo [find_ssh_key] + if {$finfo eq {}} { + set sshkey_title [mc "Generation succeded, but no keys found."] + $w.contents insert end $sshkey_output + } else { + set sshkey_title [mc "Your key is in: %s" [lindex $finfo 0]] + $w.contents insert end [lindex $finfo 1] sel + } + } + $w.contents configure -state disable +} diff --git a/git-gui/lib/tools.tcl b/git-gui/lib/tools.tcl new file mode 100644 index 0000000000..6ae63b6c7c --- /dev/null +++ b/git-gui/lib/tools.tcl @@ -0,0 +1,159 @@ +# git-gui Tools menu implementation + +proc tools_list {} { + global repo_config + + set names {} + foreach item [array names repo_config guitool.*.cmd] { + lappend names [string range $item 8 end-4] + } + return [lsort $names] +} + +proc tools_populate_all {} { + global tools_menubar tools_menutbl + global tools_tailcnt + + set mbar_end [$tools_menubar index end] + set mbar_base [expr {$mbar_end - $tools_tailcnt}] + if {$mbar_base >= 0} { + $tools_menubar delete 0 $mbar_base + } + + array unset tools_menutbl + + foreach fullname [tools_list] { + tools_populate_one $fullname + } +} + +proc tools_create_item {parent args} { + global tools_menubar tools_tailcnt + if {$parent eq $tools_menubar} { + set pos [expr {[$parent index end]-$tools_tailcnt+1}] + eval [list $parent insert $pos] $args + } else { + eval [list $parent add] $args + } +} + +proc tools_populate_one {fullname} { + global tools_menubar tools_menutbl tools_id + + if {![info exists tools_id]} { + set tools_id 0 + } + + set names [split $fullname '/'] + set parent $tools_menubar + for {set i 0} {$i < [llength $names]-1} {incr i} { + set subname [join [lrange $names 0 $i] '/'] + if {[info exists tools_menutbl($subname)]} { + set parent $tools_menutbl($subname) + } else { + set subid $parent.t$tools_id + tools_create_item $parent cascade \ + -label [lindex $names $i] -menu $subid + menu $subid + set tools_menutbl($subname) $subid + set parent $subid + incr tools_id + } + } + + tools_create_item $parent command \ + -label [lindex $names end] \ + -command [list tools_exec $fullname] +} + +proc tools_exec {fullname} { + global repo_config env current_diff_path + global current_branch is_detached + + if {[is_config_true "guitool.$fullname.needsfile"]} { + if {$current_diff_path eq {}} { + error_popup [mc "Running %s requires a selected file." $fullname] + return + } + } + + catch { unset env(ARGS) } + catch { unset env(REVISION) } + + if {[get_config "guitool.$fullname.revprompt"] ne {} || + [get_config "guitool.$fullname.argprompt"] ne {}} { + set dlg [tools_askdlg::dialog $fullname] + if {![tools_askdlg::execute $dlg]} { + return + } + } elseif {[is_config_true "guitool.$fullname.confirm"]} { + if {[ask_popup [mc "Are you sure you want to run %s?" $fullname]] ne {yes}} { + return + } + } + + set env(GIT_GUITOOL) $fullname + set env(FILENAME) $current_diff_path + if {$is_detached} { + set env(CUR_BRANCH) "" + } else { + set env(CUR_BRANCH) $current_branch + } + + set cmdline $repo_config(guitool.$fullname.cmd) + if {[is_config_true "guitool.$fullname.noconsole"]} { + tools_run_silent [list sh -c $cmdline] \ + [list tools_complete $fullname {}] + } else { + regsub {/} $fullname { / } title + set w [console::new \ + [mc "Tool: %s" $title] \ + [mc "Running: %s" $cmdline]] + console::exec $w [list sh -c $cmdline] \ + [list tools_complete $fullname $w] + } + + unset env(GIT_GUITOOL) + unset env(FILENAME) + unset env(CUR_BRANCH) + catch { unset env(ARGS) } + catch { unset env(REVISION) } +} + +proc tools_run_silent {cmd after} { + lappend cmd 2>@1 + set fd [_open_stdout_stderr $cmd] + + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [list tools_consume_input $fd $after] +} + +proc tools_consume_input {fd after} { + read $fd + if {[eof $fd]} { + fconfigure $fd -blocking 1 + if {[catch {close $fd}]} { + uplevel #0 $after 0 + } else { + uplevel #0 $after 1 + } + } +} + +proc tools_complete {fullname w {ok 1}} { + if {$w ne {}} { + console::done $w $ok + } + + if {$ok} { + set msg [mc "Tool completed succesfully: %s" $fullname] + } else { + set msg [mc "Tool failed: %s" $fullname] + } + + if {[is_config_true "guitool.$fullname.norescan"]} { + ui_status $msg + } else { + rescan [list ui_status $msg] + } +} diff --git a/git-gui/lib/tools_dlg.tcl b/git-gui/lib/tools_dlg.tcl new file mode 100644 index 0000000000..5f7f08e239 --- /dev/null +++ b/git-gui/lib/tools_dlg.tcl @@ -0,0 +1,421 @@ +# git-gui Tools menu dialogs + +class tools_add { + +field w ; # widget path +field w_name ; # new remote name widget +field w_cmd ; # new remote location widget + +field name {}; # name of the tool +field command {}; # command to execute +field add_global 0; # add to the --global config +field no_console 0; # disable using the console +field needs_file 0; # ensure filename is set +field confirm 0; # ask for confirmation +field ask_branch 0; # ask for a revision +field ask_args 0; # ask for additional args + +constructor dialog {} { + global repo_config + + make_toplevel top w + wm title $top [append "[appname] ([reponame]): " [mc "Add Tool"]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + label $w.header -text [mc "Add New Tool Command"] -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + checkbutton $w.buttons.global \ + -text [mc "Add globally"] \ + -variable @add_global + pack $w.buttons.global -side left -padx 5 + button $w.buttons.create -text [mc Add] \ + -default active \ + -command [cb _add] + pack $w.buttons.create -side right + button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + labelframe $w.desc -text [mc "Tool Details"] + + label $w.desc.name_cmnt -anchor w\ + -text [mc "Use '/' separators to create a submenu tree:"] + grid x $w.desc.name_cmnt -sticky we -padx {0 5} -pady {0 2} + label $w.desc.name_l -text [mc "Name:"] + set w_name $w.desc.name_t + entry $w_name \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @name \ + -validate key \ + -validatecommand [cb _validate_name %d %S] + grid $w.desc.name_l $w_name -sticky we -padx {0 5} + + label $w.desc.cmd_l -text [mc "Command:"] + set w_cmd $w.desc.cmd_t + entry $w_cmd \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @command + grid $w.desc.cmd_l $w_cmd -sticky we -padx {0 5} -pady {0 3} + + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + + checkbutton $w.confirm \ + -text [mc "Show a dialog before running"] \ + -variable @confirm -command [cb _check_enable_dlg] + + labelframe $w.dlg -labelwidget $w.confirm + + checkbutton $w.dlg.askbranch \ + -text [mc "Ask the user to select a revision (sets \$REVISION)"] \ + -variable @ask_branch -state disabled + pack $w.dlg.askbranch -anchor w -padx 15 + + checkbutton $w.dlg.askargs \ + -text [mc "Ask the user for additional arguments (sets \$ARGS)"] \ + -variable @ask_args -state disabled + pack $w.dlg.askargs -anchor w -padx 15 + + pack $w.dlg -anchor nw -fill x -pady {0 8} -padx 5 + + checkbutton $w.noconsole \ + -text [mc "Don't show the command output window"] \ + -variable @no_console + pack $w.noconsole -anchor w -padx 5 + + checkbutton $w.needsfile \ + -text [mc "Run only if a diff is selected (\$FILENAME not empty)"] \ + -variable @needs_file + pack $w.needsfile -anchor w -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _add]\;break + tkwait window $w +} + +method _check_enable_dlg {} { + if {$confirm} { + $w.dlg.askbranch configure -state normal + $w.dlg.askargs configure -state normal + } else { + $w.dlg.askbranch configure -state disabled + $w.dlg.askargs configure -state disabled + } +} + +method _add {} { + global repo_config + + if {$name eq {}} { + error_popup [mc "Please supply a name for the tool."] + focus $w_name + return + } + + set item "guitool.$name.cmd" + + if {[info exists repo_config($item)]} { + error_popup [mc "Tool '%s' already exists." $name] + focus $w_name + return + } + + set cmd [list git config] + if {$add_global} { lappend cmd --global } + set items {} + if {$no_console} { lappend items "guitool.$name.noconsole" } + if {$needs_file} { lappend items "guitool.$name.needsfile" } + if {$confirm} { + if {$ask_args} { lappend items "guitool.$name.argprompt" } + if {$ask_branch} { lappend items "guitool.$name.revprompt" } + if {!$ask_args && !$ask_branch} { + lappend items "guitool.$name.confirm" + } + } + + if {[catch { + eval $cmd [list $item $command] + foreach citem $items { eval $cmd [list $citem yes] } + } err]} { + error_popup [mc "Could not add tool:\n%s" $err] + } else { + set repo_config($item) $command + foreach citem $items { set repo_config($citem) yes } + + tools_populate_all + } + + destroy $w +} + +method _validate_name {d S} { + if {$d == 1} { + if {[regexp {[~?*&\[\0\"\\\{]} $S]} { + return 0 + } + } + return 1 +} + +method _visible {} { + grab $w + $w_name icursor end + focus $w_name +} + +} + +class tools_remove { + +field w ; # widget path +field w_names ; # name list + +constructor dialog {} { + global repo_config global_config system_config + + load_config 1 + + make_toplevel top w + wm title $top [append "[appname] ([reponame]): " [mc "Remove Tool"]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + label $w.header -text [mc "Remove Tool Commands"] -font font_uibold + pack $w.header -side top -fill x + + frame $w.buttons + button $w.buttons.create -text [mc Remove] \ + -default active \ + -command [cb _remove] + pack $w.buttons.create -side right + button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + frame $w.list + set w_names $w.list.l + listbox $w_names \ + -height 10 \ + -width 30 \ + -selectmode extended \ + -exportselection false \ + -yscrollcommand [list $w.list.sby set] + scrollbar $w.list.sby -command [list $w.list.l yview] + pack $w.list.sby -side right -fill y + pack $w.list.l -side left -fill both -expand 1 + pack $w.list -fill both -expand 1 -pady 5 -padx 5 + + set local_cnt 0 + foreach fullname [tools_list] { + # Cannot delete system tools + if {[info exists system_config(guitool.$fullname.cmd)]} continue + + $w_names insert end $fullname + if {![info exists global_config(guitool.$fullname.cmd)]} { + $w_names itemconfigure end -foreground blue + incr local_cnt + } + } + + if {$local_cnt > 0} { + label $w.colorlbl -foreground blue \ + -text [mc "(Blue denotes repository-local tools)"] + pack $w.colorlbl -fill x -pady 5 -padx 5 + } + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _remove]\;break + tkwait window $w +} + +method _remove {} { + foreach i [$w_names curselection] { + set name [$w_names get $i] + + catch { git config --remove-section guitool.$name } + catch { git config --global --remove-section guitool.$name } + } + + load_config 0 + tools_populate_all + + destroy $w +} + +method _visible {} { + grab $w + focus $w_names +} + +} + +class tools_askdlg { + +field w ; # widget path +field w_rev {}; # revision browser +field w_args {}; # arguments + +field is_ask_args 0; # has arguments field +field is_ask_revs 0; # has revision browser + +field is_ok 0; # ok to start +field argstr {}; # arguments + +constructor dialog {fullname} { + global M1B + + set title [get_config "guitool.$fullname.title"] + if {$title eq {}} { + regsub {/} $fullname { / } title + } + + make_toplevel top w -autodelete 0 + wm title $top [append "[appname] ([reponame]): " $title] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + set prompt [get_config "guitool.$fullname.prompt"] + if {$prompt eq {}} { + set command [get_config "guitool.$fullname.cmd"] + set prompt [mc "Run Command: %s" $command] + } + + label $w.header -text $prompt -font font_uibold + pack $w.header -side top -fill x + + set argprompt [get_config "guitool.$fullname.argprompt"] + set revprompt [get_config "guitool.$fullname.revprompt"] + + set is_ask_args [expr {$argprompt ne {}}] + set is_ask_revs [expr {$revprompt ne {}}] + + if {$is_ask_args} { + if {$argprompt eq {yes} || $argprompt eq {true} || $argprompt eq {1}} { + set argprompt [mc "Arguments"] + } + + labelframe $w.arg -text $argprompt + + set w_args $w.arg.txt + entry $w_args \ + -borderwidth 1 \ + -relief sunken \ + -width 40 \ + -textvariable @argstr + pack $w_args -padx 5 -pady 5 -fill both + pack $w.arg -anchor nw -fill both -pady 5 -padx 5 + } + + if {$is_ask_revs} { + if {$revprompt eq {yes} || $revprompt eq {true} || $revprompt eq {1}} { + set revprompt [mc "Revision"] + } + + if {[is_config_true "guitool.$fullname.revunmerged"]} { + set w_rev [::choose_rev::new_unmerged $w.rev $revprompt] + } else { + set w_rev [::choose_rev::new $w.rev $revprompt] + } + + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + } + + frame $w.buttons + if {$is_ask_revs} { + button $w.buttons.visualize \ + -text [mc Visualize] \ + -command [cb _visualize] + pack $w.buttons.visualize -side left + } + button $w.buttons.ok \ + -text [mc OK] \ + -command [cb _start] + pack $w.buttons.ok -side right + button $w.buttons.cancel \ + -text [mc "Cancel"] \ + -command [cb _cancel] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + bind $w <$M1B-Key-Return> [cb _start] + bind $w <Key-Return> [cb _start] + bind $w <Key-Escape> [cb _cancel] + wm protocol $w WM_DELETE_WINDOW [cb _cancel] + + bind $w <Visibility> [cb _visible] + return $this +} + +method execute {} { + tkwait window $w + set rv $is_ok + delete_this + return $rv +} + +method _visible {} { + grab $w + if {$is_ask_args} { + focus $w_args + } elseif {$is_ask_revs} { + $w_rev focus_filter + } +} + +method _cancel {} { + wm protocol $w WM_DELETE_WINDOW {} + destroy $w +} + +method _rev {} { + if {[catch {$w_rev commit_or_die}]} { + return {} + } + return [$w_rev get] +} + +method _visualize {} { + global current_branch + set rev [_rev $this] + if {$rev ne {}} { + do_gitk [list --left-right "$current_branch...$rev"] + } +} + +method _start {} { + global env + + if {$is_ask_revs} { + set name [_rev $this] + if {$name eq {}} { + return + } + set env(REVISION) $name + } + + if {$is_ask_args} { + set env(ARGS) $argstr + } + + set is_ok 1 + _cancel $this +} + +} diff --git a/git-gui/lib/transport.tcl b/git-gui/lib/transport.tcl index 8e6a9d0a60..e419d7810a 100644 --- a/git-gui/lib/transport.tcl +++ b/git-gui/lib/transport.tcl @@ -135,7 +135,7 @@ proc do_push_anywhere {} { set push_urltype url } radiobutton $w.dest.url_r \ - -text [mc "Arbitrary URL:"] \ + -text [mc "Arbitrary Location:"] \ -value url \ -variable push_urltype entry $w.dest.url_t \ |