diff options
Diffstat (limited to 'git-gui/lib')
43 files changed, 12715 insertions, 0 deletions
| diff --git a/git-gui/lib/about.tcl b/git-gui/lib/about.tcl new file mode 100644 index 0000000000..122ebfb71d --- /dev/null +++ b/git-gui/lib/about.tcl @@ -0,0 +1,70 @@ +# git-gui about git-gui dialog +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_about {} { +	global appvers copyright oguilib +	global tcl_patchLevel tk_patchLevel +	global ui_comm_spell + +	set w .about_dialog +	Dialog $w +	wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + +	pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10 +	ttk::label $w.header -text [mc "About %s" [appname]] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.close -text {Close} \ +		-default active \ +		-command [list destroy $w] +	pack $w.buttons.close -side right +	pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + +	paddedlabel $w.desc \ +		-text "[mc "git-gui - a graphical user interface for Git."]\n$copyright" +	pack $w.desc -side top -fill x -padx 5 -pady 5 + +	set v {} +	append v "git-gui version $appvers\n" +	append v "[git version]\n" +	append v "\n" +	if {$tcl_patchLevel eq $tk_patchLevel} { +		append v "Tcl/Tk version $tcl_patchLevel" +	} else { +		append v "Tcl version $tcl_patchLevel" +		append v ", Tk version $tk_patchLevel" +	} +	if {[info exists ui_comm_spell] +		&& [$ui_comm_spell version] ne {}} { +		append v "\n" +		append v [$ui_comm_spell version] +	} + +	set d {} +	append d "git wrapper: $::_git\n" +	append d "git exec dir: [git --exec-path]\n" +	append d "git-gui lib: $oguilib" + +	paddedlabel $w.vers -text $v +	pack $w.vers -side top -fill x -padx 5 -pady 5 + +	paddedlabel $w.dirs -text $d +	pack $w.dirs -side top -fill x -padx 5 -pady 5 + +	menu $w.ctxm -tearoff 0 +	$w.ctxm add command \ +		-label {Copy} \ +		-command " +		clipboard clear +		clipboard append -format STRING -type STRING -- \[$w.vers cget -text\] +	" + +	bind $w <Visibility> "grab $w; focus $w.buttons.close" +	bind $w <Key-Escape> "destroy $w" +	bind $w <Key-Return> "destroy $w" +	bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w" +	wm title $w "About [appname]" +	tkwait window $w +} diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl new file mode 100644 index 0000000000..4477b84eae --- /dev/null +++ b/git-gui/lib/blame.tcl @@ -0,0 +1,1370 @@ +# git-gui blame viewer +# Copyright (C) 2006, 2007 Shawn Pearce + +class blame { + +image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +# Persistent data (survives loads) +# +field history {}; # viewer history: {commit path} +field header    ; # array commit,key -> header field + +# Tk UI control paths +# +field w          ; # top window in this viewer +field w_back     ; # our back button +field w_path     ; # label showing the current file path +field w_columns  ; # list of all column widgets in the viewer +field w_line     ; # text column: all line numbers +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 gotoline   ; # line goto mini-dialog frame +field status     ; # status mega-widget instance +field status_operation ; # operation displayed by status mega-widget +field old_height ; # last known height of $w.file_pane + + +# Tk UI colors +# +variable active_color #c0edc5 +variable group_colors { +	#d6d6d6 +	#e1e1e1 +	#ececec +} + +# Current blame data; cleared/reset on each load +# +field commit               ; # input commit to blame +field path                 ; # input filename to view in $commit + +field current_fd        {} ; # background process running +field highlight_line    -1 ; # current line selected +field highlight_column  {} ; # current commit column selected +field highlight_commit  {} ; # sha1 of commit selected + +field total_lines       0  ; # total length of file +field blame_lines       0  ; # number of lines computed +field amov_data            ; # list of {commit origfile origline} +field asim_data            ; # list of {commit origfile origline} + +field r_commit             ; # commit currently being parsed +field r_orig_line          ; # original line number +field r_final_line         ; # final line number +field r_line_count         ; # lines in this region + +field tooltip_wm        {} ; # Current tooltip toplevel, if open +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 i_jump} { +	global cursor_ptr M1B M1T +	variable active_color +	variable group_colors + +	set commit $i_commit +	set path   $i_path + +	make_toplevel top w +	wm title $top [mc "%s (%s): File Viewer" [appname] [reponame]] + +	set font_w [font measure font_diff "0"] + +	gold_frame $w.header +	tlabel $w.header.commit_l \ +		-text [mc "Commit:"] \ +		-background gold \ +		-foreground black \ +		-anchor w \ +		-justify left +	set w_back $w.header.commit_b +	tlabel $w_back \ +		-image ::blame::img_back_arrow \ +		-borderwidth 0 \ +		-relief flat \ +		-state disabled \ +		-background gold \ +		-foreground black \ +		-activebackground gold +	bind $w_back <Button-1> " +		if {\[$w_back cget -state\] eq {normal}} { +			[cb _history_menu] +		} +		" +	tlabel $w.header.commit \ +		-textvariable @commit \ +		-background gold \ +		-foreground black \ +		-anchor w \ +		-justify left +	tlabel $w.header.path_l \ +		-text [mc "File:"] \ +		-background gold \ +		-foreground black \ +		-anchor w \ +		-justify left +	set w_path $w.header.path +	tlabel $w_path \ +		-background gold \ +		-foreground black \ +		-anchor w \ +		-justify left +	pack $w.header.commit_l -side left +	pack $w_back -side left +	pack $w.header.commit -side left +	pack $w_path -fill x -side right +	pack $w.header.path_l -side right + +	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 \ +		-height 100 \ +		-width 100 +	$w.file_pane add $w.file_pane.cm \ +		-sticky nsew \ +		-minsize 25 \ +		-height 25 \ +		-width 100 + +	set w_line $w.file_pane.out.linenumber_t +	text $w_line \ +		-takefocus 0 \ +		-highlightthickness 0 \ +		-padx 0 -pady 0 \ +		-background white \ +		-foreground black \ +		-borderwidth 0 \ +		-state disabled \ +		-wrap none \ +		-height 40 \ +		-width 6 \ +		-font font_diff +	$w_line tag conf linenumber -justify right -rmargin 5 + +	set w_amov $w.file_pane.out.amove_t +	text $w_amov \ +		-takefocus 0 \ +		-highlightthickness 0 \ +		-padx 0 -pady 0 \ +		-background white \ +		-foreground black \ +		-borderwidth 0 \ +		-state disabled \ +		-wrap none \ +		-height 40 \ +		-width 5 \ +		-font font_diff +	$w_amov tag conf author_abbr -justify right -rmargin 5 +	$w_amov tag conf curr_commit +	$w_amov tag conf prior_commit -foreground blue -underline 1 +	$w_amov tag bind prior_commit \ +		<Button-1> \ +		"[cb _load_commit $w_amov @amov_data @%x,%y];break" + +	set w_asim $w.file_pane.out.asimple_t +	text $w_asim \ +		-takefocus 0 \ +		-highlightthickness 0 \ +		-padx 0 -pady 0 \ +		-background white \ +		-foreground black \ +		-borderwidth 0 \ +		-state disabled \ +		-wrap none \ +		-height 40 \ +		-width 4 \ +		-font font_diff +	$w_asim tag conf author_abbr -justify right +	$w_asim tag conf curr_commit +	$w_asim tag conf prior_commit -foreground blue -underline 1 +	$w_asim tag bind prior_commit \ +		<Button-1> \ +		"[cb _load_commit $w_asim @asim_data @%x,%y];break" + +	set w_file $w.file_pane.out.file_t +	text $w_file \ +		-takefocus 0 \ +		-highlightthickness 0 \ +		-padx 0 -pady 0 \ +		-background white \ +		-foreground black \ +		-borderwidth 0 \ +		-state disabled \ +		-wrap none \ +		-height 40 \ +		-width 80 \ +		-xscrollcommand [list $w.file_pane.out.sbx set] \ +		-font font_diff +		$w_file configure -inactiveselectbackground darkblue + +	$w_file tag conf found \ +		-background yellow + +	set w_columns [list $w_amov $w_asim $w_line $w_file] + +	ttk::scrollbar $w.file_pane.out.sbx \ +		-orient h \ +		-command [list $w_file xview] +	ttk::scrollbar $w.file_pane.out.sby \ +		-orient v \ +		-command [list scrollbar2many $w_columns yview] +	eval grid $w_columns $w.file_pane.out.sby -sticky nsew +	grid conf \ +		$w.file_pane.out.sbx \ +		-column 0 \ +		-columnspan [expr {[llength $w_columns] + 1}] \ +		-sticky we +	grid columnconfigure \ +		$w.file_pane.out \ +		[expr {[llength $w_columns] - 1}] \ +		-weight 1 +	grid rowconfigure $w.file_pane.out 0 -weight 1 + +	set finder [::searchbar::new \ +		$w.file_pane.out.ff $w_file \ +		-column 0 \ +		-columnspan [expr {[llength $w_columns] + 1}] \ +		] + +	set gotoline [::linebar::new \ +		$w.file_pane.out.lf $w_file \ +		-column 0 \ +		-columnspan [expr {[llength $w_columns] + 1}] \ +		] + +	set w_cviewer $w.file_pane.cm.t +	text $w_cviewer \ +		-background white \ +		-foreground black \ +		-borderwidth 0 \ +		-state disabled \ +		-wrap none \ +		-height 10 \ +		-width 80 \ +		-xscrollcommand [list $w.file_pane.cm.sbx set] \ +		-yscrollcommand [list $w.file_pane.cm.sby set] \ +		-font font_diff +	$w_cviewer tag conf still_loading \ +		-font font_uiitalic \ +		-justify center +	$w_cviewer tag conf header_key \ +		-tabs {3c} \ +		-background $active_color \ +		-font font_uibold +	$w_cviewer tag conf header_val \ +		-background $active_color \ +		-font font_ui +	$w_cviewer tag raise sel +	ttk::scrollbar $w.file_pane.cm.sbx \ +		-orient h \ +		-command [list $w_cviewer xview] +	ttk::scrollbar $w.file_pane.cm.sby \ +		-orient v \ +		-command [list $w_cviewer yview] +	pack $w.file_pane.cm.sby -side right -fill y +	pack $w.file_pane.cm.sbx -side bottom -fill x +	pack $w_cviewer -expand 1 -fill both + +	set status [::status_bar::new $w.status] +	set status_operation {} + +	menu $w.ctxm -tearoff 0 +	$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 [cb _show_finder] +	$w.ctxm add command \ +		-label [mc "Goto Line..."] \ +		-accelerator "Ctrl-G" \ +		-command [cb _show_linebar] +	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 ::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] +			focus $i +		" +		bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y] +		bind $i <Any-Enter>   [cb _hide_tooltip] +		bind $i <Any-Leave>   [cb _hide_tooltip] +		bind $i <Deactivate>  [cb _hide_tooltip] +		bind_button3 $i " +			[cb _hide_tooltip] +			set cursorX %x +			set cursorY %y +			set cursorW %W +			tk_popup $w.ctxm %X %Y +		" +		bind $i <Shift-Tab> "[list focus $w_cviewer];break" +		bind $i <Tab>       "[cb _focus_search $w_cviewer];break" +	} + +	foreach i [concat $w_columns $w_cviewer] { +		bind $i <Key-Up>        {catch {%W yview scroll -1 units};break} +		bind $i <Key-Down>      {catch {%W yview scroll  1 units};break} +		bind $i <Key-Left>      {catch {%W xview scroll -1 units};break} +		bind $i <Key-Right>     {catch {%W xview scroll  1 units};break} +		bind $i <Key-k>         {catch {%W yview scroll -1 units};break} +		bind $i <Key-j>         {catch {%W yview scroll  1 units};break} +		bind $i <Key-h>         {catch {%W xview scroll -1 units};break} +		bind $i <Key-l>         {catch {%W xview scroll  1 units};break} +		bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break} +		bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};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> [cb _focus_search $w_file] +	bind $top       <F7>         [cb _show_finder] +	bind $top       <Key-slash>  [cb _show_finder] +	bind $top    <Control-Key-s> [cb _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] +	bind $top    <Control-Key-g> [cb _show_linebar] +	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 +	grid configure $w.status -sticky ew +	grid columnconfigure $top 0 -weight 1 +	grid rowconfigure $top 0 -weight 0 +	grid rowconfigure $top 1 -weight 1 +	grid rowconfigure $top 2 -weight 0 + +	set req_w [winfo reqwidth  $top] +	set req_h [winfo reqheight $top] +	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 + +	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.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 _handle_destroy %W] + +	_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 {} { +	if {$current_fd ne {}} { +		kill_file_process $current_fd +		catch {close $current_fd} +		set current_fd {} +	} +} + +method _load {jump} { +	variable group_colors +	global hashlength + +	_hide_tooltip $this + +	if {$total_lines != 0 || $current_fd ne {}} { +		_kill $this + +		foreach i $w_columns { +			$i conf -state normal +			$i delete 0.0 end +			foreach g [$i tag names] { +				if {[regexp [string map "@@ $hashlength" {^g[0-9a-f]{@@}$}] $g]} { +					$i tag delete $g +				} +			} +			$i conf -state disabled +		} + +		$w_cviewer conf -state normal +		$w_cviewer delete 0.0 end +		$w_cviewer conf -state disabled + +		set highlight_line -1 +		set highlight_column {} +		set highlight_commit {} +		set total_lines 0 +	} + +	if {$history eq {}} { +		$w_back conf -state disabled +	} else { +		$w_back conf -state normal +	} + +	# Index 0 is always empty.  There is never line 0 as +	# we use only 1 based lines, as that matches both with +	# git-blame output and with Tk's text widget. +	# +	set amov_data [list [list]] +	set asim_data [list [list]] + +	$status show [mc "Reading %s..." "$commit:[escape_path $path]"] +	$w_path conf -text [escape_path $path] + +	set do_textconv 0 +	if {![is_config_false gui.textconv]} { +		set filter [gitattr $path diff set] +		set textconv [get_config [join [list diff $filter textconv] .]] +		if {$filter ne {set} && $textconv ne {}} { +			set do_textconv 1 +		} +	} +	if {$commit eq {}} { +		if {$do_textconv ne 0} { +			set fd [open_cmd_pipe $textconv $path] +		} else { +			set fd [safe_open_file $path r] +		} +	} else { +		if {$do_textconv ne 0} { +			set fd [git_read [list cat-file --textconv "$commit:$path"]] +		} else { +			set fd [git_read [list cat-file blob "$commit:$path"]] +		} +	} +	fconfigure $fd \ +		-blocking 0 \ +		-encoding [get_path_encoding $path] +	fileevent $fd readable [cb _read_file $fd $jump] +	set current_fd $fd +} + +method _history_menu {} { +	global hashlength + +	set m $w.backmenu +	if {[winfo exists $m]} { +		$m delete 0 end +	} else { +		menu $m -tearoff 0 +	} + +	for {set i [expr {[llength $history] - 1}] +		} {$i >= 0} {incr i -1} { +		set e [lindex $history $i] +		set c [lindex $e 0] +		set f [lindex $e 1] + +		if {[regexp [string map "@@ $hashlength" {^[0-9a-f]{@@}$}] $c]} { +			set t [string range $c 0 8]... +		} elseif {$c eq {}} { +			set t {Working Directory} +		} else { +			set t $c +		} +		if {![catch {set summary $header($c,summary)}]} { +			append t " $summary" +			if {[string length $t] > 70} { +				set t [string range $t 0 66]... +			} +		} + +		$m add command -label $t -command [cb _goback $i] +	} +	set X [winfo rootx $w_back] +	set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}] +	tk_popup $m $X $Y +} + +method _goback {i} { +	set dat [lindex $history $i] +	set history [lrange $history 0 [expr {$i - 1}]] +	set commit [lindex $dat 0] +	set path [lindex $dat 1] +	_load $this [lrange $dat 2 5] +} + +method _read_file {fd jump} { +	if {$fd ne $current_fd} { +		catch {close $fd} +		return +	} + +	foreach i $w_columns {$i conf -state normal} +	while {[gets $fd line] >= 0} { +		regsub "\r\$" $line {} line +		incr total_lines +		lappend amov_data {} +		lappend asim_data {} + +		if {$total_lines > 1} { +			foreach i $w_columns {$i insert end "\n"} +		} + +		$w_line insert end "$total_lines" linenumber +		$w_file insert end "$line" +	} + +	set ln_wc [expr {[string length $total_lines] + 2}] +	if {[$w_line cget -width] < $ln_wc} { +		$w_line conf -width $ln_wc +	} + +	foreach i $w_columns {$i conf -state disabled} + +	if {[eof $fd]} { +		fconfigure $fd -blocking 1; # enable error reporting on close +		if {[catch {close $fd} err]} { +			tk_messageBox -icon error -title [mc Error] \ +				-message $err +		} + +		# If we don't force Tk to update the widgets *right now* +		# none of our jump commands will cause a change in the UI. +		# +		update + +		if {[llength $jump] == 1} { +			set highlight_line [lindex $jump 0] +			$w_file see "$highlight_line.0" +		} elseif {[llength $jump] == 4} { +			set highlight_column [lindex $jump 0] +			set highlight_line [lindex $jump 1] +			$w_file xview moveto [lindex $jump 2] +			$w_file yview moveto [lindex $jump 3] +		} + +		_exec_blame $this $w_asim @asim_data \ +			[list] \ +			[mc "Loading copy/move tracking annotations..."] +	} +} ifdeleted { catch {close $fd} } + +method _exec_blame {cur_w cur_d options cur_s} { +	lappend options --incremental --encoding=utf-8 +	if {$commit eq {}} { +		lappend options --contents $path +	} else { +		lappend options $commit +	} + +	# We may recurse in from another call to _exec_blame and already have +	# a status operation. +	if {$status_operation == {}} { +		set status_operation [$status start \ +			$cur_s \ +			[mc "lines annotated"]] +	} else { +		$status_operation restart $cur_s +	} + +	lappend options -- $path +	set fd [git_read_nice [concat blame $options]] +	fconfigure $fd -blocking 0 -encoding utf-8 +	fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d] +	set current_fd $fd +	set blame_lines 0 +} + +method _read_blame {fd cur_w cur_d} { +	upvar #0 $cur_d line_data +	variable group_colors +	global hashlength nullid + +	if {$fd ne $current_fd} { +		catch {close $fd} +		return +	} + +	$cur_w conf -state normal +	while {[gets $fd line] >= 0} { +		if {[regexp [string map "@@ $hashlength" {^([a-z0-9]{@@}) (\d+) (\d+) (\d+)$}] $line line \ +			cmit original_line final_line line_count]} { +			set r_commit     $cmit +			set r_orig_line  $original_line +			set r_final_line $final_line +			set r_line_count $line_count +		} elseif {[string match {filename *} $line]} { +			set file [string range $line 9 end] +			set n    $r_line_count +			set lno  $r_final_line +			set oln  $r_orig_line +			set cmit $r_commit + +			if {$cmit eq $nullid} { +				set commit_abbr work +				set commit_type curr_commit +			} elseif {$cmit eq $commit} { +				set commit_abbr this +				set commit_type curr_commit +			} else { +				set commit_type prior_commit +				set commit_abbr [string range $cmit 0 3] +			} + +			set author_abbr {} +			set a_name {} +			catch {set a_name $header($cmit,author)} +			while {$a_name ne {}} { +				if {$author_abbr ne {} +					&& [string index $a_name 0] eq {'}} { +					regsub {^'[^']+'\s+} $a_name {} a_name +				} +				if {![regexp {^([[:upper:]])} $a_name _a]} break +				append author_abbr $_a +				unset _a +				if {![regsub \ +					{^[[:upper:]][^\s]*\s+} \ +					$a_name {} a_name ]} break +			} +			if {$author_abbr eq {}} { +				set author_abbr { |} +			} else { +				set author_abbr [string range $author_abbr 0 3] +			} +			unset a_name + +			set first_lno $lno +			while { +			   $first_lno > 1 +			&& $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0] +			&& $file eq [lindex $line_data [expr {$first_lno - 1}] 1] +			} { +				incr first_lno -1 +			} + +			set color {} +			if {$first_lno < $lno} { +				foreach g [$w_file tag names $first_lno.0] { +					if {[regexp {^color[0-9]+$} $g]} { +						set color $g +						break +					} +				} +			} else { +				set i [lsort [concat \ +					[$w_file tag names "[expr {$first_lno - 1}].0"] \ +					[$w_file tag names "[expr {$lno + $n}].0"] \ +					]] +				for {set g 0} {$g < [llength $group_colors]} {incr g} { +					if {[lsearch -sorted -exact $i color$g] == -1} { +						set color color$g +						break +					} +				} +			} +			if {$color eq {}} { +				set color color0 +			} + +			while {$n > 0} { +				set lno_e "$lno.0 lineend + 1c" +				if {[lindex $line_data $lno] ne {}} { +					set g [lindex $line_data $lno 0] +					foreach i $w_columns { +						$i tag remove g$g $lno.0 $lno_e +					} +				} +				lset line_data $lno [list $cmit $file $oln] + +				$cur_w delete $lno.0 "$lno.0 lineend" +				if {$lno == $first_lno} { +					$cur_w insert $lno.0 $commit_abbr $commit_type +				} elseif {$lno == [expr {$first_lno + 1}]} { +					$cur_w insert $lno.0 $author_abbr author_abbr +				} else { +					$cur_w insert $lno.0 { |} +				} + +				foreach i $w_columns { +					if {$cur_w eq $w_amov} { +						for {set g 0} \ +							{$g < [llength $group_colors]} \ +							{incr g} { +							$i tag remove color$g $lno.0 $lno_e +						} +						$i tag add $color $lno.0 $lno_e +					} +					$i tag add g$cmit $lno.0 $lno_e +				} + +				if {$highlight_column eq $cur_w} { +					if {$highlight_line == -1 +					 && [lindex [$w_file yview] 0] == 0} { +						$w_file see $lno.0 +						set highlight_line $lno +					} +					if {$highlight_line == $lno} { +						_showcommit $this $cur_w $lno +					} +				} + +				incr n -1 +				incr lno +				incr oln +				incr blame_lines +			} + +			while { +			   $cmit eq [lindex $line_data $lno 0] +			&& $file eq [lindex $line_data $lno 1] +			} { +				$cur_w delete $lno.0 "$lno.0 lineend" + +				if {$lno == $first_lno} { +					$cur_w insert $lno.0 $commit_abbr $commit_type +				} elseif {$lno == [expr {$first_lno + 1}]} { +					$cur_w insert $lno.0 $author_abbr author_abbr +				} else { +					$cur_w insert $lno.0 { |} +				} + +				if {$cur_w eq $w_amov} { +					foreach i $w_columns { +						for {set g 0} \ +							{$g < [llength $group_colors]} \ +							{incr g} { +							$i tag remove color$g $lno.0 $lno_e +						} +						$i tag add $color $lno.0 $lno_e +					} +				} + +				incr lno +			} + +		} elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} { +			set header($r_commit,$key) $data +		} +	} +	$cur_w conf -state disabled + +	if {[eof $fd]} { +		close $fd +		if {$cur_w eq $w_asim} { +			# Switches for original location detection +			set threshold [get_config gui.copyblamethreshold] +			set original_options [list "-C$threshold"] + +			if {![is_config_true gui.fastcopyblame]} { +				# thorough copy search; insert before the threshold +				set original_options [linsert $original_options 0 -C] +			} +			lappend original_options -w ; # ignore indentation changes + +			_exec_blame $this $w_amov @amov_data \ +				$original_options \ +				[mc "Loading original location annotations..."] +		} else { +			set current_fd {} +			$status_operation stop [mc "Annotation complete."] +			set status_operation {} +		} +	} else { +		$status_operation update $blame_lines $total_lines +	} +} ifdeleted { catch {close $fd} } + +method _find_commit_bound {data_list start_idx delta} { +	upvar #0 $data_list line_data +	set pos $start_idx +	set limit       [expr {[llength $line_data] - 1}] +	set base_commit [lindex $line_data $pos 0] + +	while {$pos > 0 && $pos < $limit} { +		set new_pos [expr {$pos + $delta}] +		if {[lindex $line_data $new_pos 0] ne $base_commit} { +			return $pos +		} + +		set pos $new_pos +	} + +	return $pos +} + +method _fullcopyblame {} { +	if {$current_fd ne {}} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [mc "Busy"] \ +			-message [mc "Annotation process is already running."] + +		return +	} + +	# Switches for original location detection +	set threshold [get_config gui.copyblamethreshold] +	set original_options [list -C -C "-C$threshold"] + +	lappend original_options -w ; # ignore indentation changes + +	# Find the line range +	set pos @$::cursorX,$::cursorY +	set lno [lindex [split [$::cursorW index $pos] .] 0] +	set min_amov_lno [_find_commit_bound $this @amov_data $lno -1] +	set max_amov_lno [_find_commit_bound $this @amov_data $lno 1] +	set min_asim_lno [_find_commit_bound $this @asim_data $lno -1] +	set max_asim_lno [_find_commit_bound $this @asim_data $lno 1] + +	if {$min_asim_lno < $min_amov_lno} { +		set min_amov_lno $min_asim_lno +	} + +	if {$max_asim_lno > $max_amov_lno} { +		set max_amov_lno $max_asim_lno +	} + +	lappend original_options -L "$min_amov_lno,$max_amov_lno" + +	# Clear lines +	for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} { +		lset amov_data $i [list ] +	} + +	# Start the back-end process +	_exec_blame $this $w_amov @amov_data \ +		$original_options \ +		[mc "Running thorough copy detection..."] +} + +method _click {cur_w pos} { +	set lno [lindex [split [$cur_w index $pos] .] 0] +	_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 {}} { +		_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 + +	if {$highlight_commit ne {}} { +		foreach i $w_columns { +			$i tag conf g$highlight_commit -background {} +			$i tag lower g$highlight_commit +		} +	} + +	if {$cur_w eq $w_asim} { +		set dat [lindex $asim_data $lno] +		set highlight_column $w_asim +	} else { +		set dat [lindex $amov_data $lno] +		set highlight_column $w_amov +	} + +	$w_cviewer conf -state normal +	$w_cviewer delete 0.0 end + +	if {$dat eq {}} { +		set cmit {} +		$w_cviewer insert end [mc "Loading annotation..."] still_loading +	} else { +		set cmit [lindex $dat 0] +		set file [lindex $dat 1] + +		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 {} +		set author_email {} +		set author_time {} +		catch {set author_name $header($cmit,author)} +		catch {set author_email $header($cmit,author-mail)} +		catch {set author_time [format_date $header($cmit,author-time)]} + +		set committer_name {} +		set committer_email {} +		set committer_time {} +		catch {set committer_name $header($cmit,committer)} +		catch {set committer_email $header($cmit,committer-mail)} +		catch {set committer_time [format_date $header($cmit,committer-time)]} + +		if {[catch {set msg $header($cmit,message)}]} { +			set msg {} +			catch { +				set fd [git_read [list cat-file commit $cmit]] +				fconfigure $fd -encoding iso8859-1 +				# By default commits are assumed to be in utf-8 +				set enc utf-8 +				while {[gets $fd line] > 0} { +					if {[string match {encoding *} $line]} { +						set enc [string tolower [string range $line 9 end]] +					} +				} +				set msg [read $fd] +				close $fd + +				set enc [tcl_encoding $enc] +				if {$enc ne {}} { +					set msg [convertfrom $enc $msg] +				} +				set msg [string trim $msg] +			} +			set header($cmit,message) $msg +		} + +		$w_cviewer insert end "commit $cmit\n" header_key +		$w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key +		$w_cviewer insert end "$author_name $author_email" header_val +		$w_cviewer insert end "  $author_time\n" header_val + +		$w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key +		$w_cviewer insert end "$committer_name $committer_email" header_val +		$w_cviewer insert end "  $committer_time\n" header_val + +		if {$file ne $path} { +			$w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key +			$w_cviewer insert end "[escape_path $file]\n" header_val +		} + +		$w_cviewer insert end "\n$msg" +	} +	$w_cviewer conf -state disabled + +	set highlight_line $lno +	set highlight_commit $cmit + +	if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} { +		_hide_tooltip $this +	} +} + +method _get_click_amov_info {} { +	set pos @$::cursorX,$::cursorY +	set lno [lindex [split [$::cursorW index $pos] .] 0] +	return [lindex $amov_data $lno] +} + +method _copycommit {} { +	set dat [_get_click_amov_info $this] +	if {$dat ne {}} { +		clipboard clear +		clipboard append \ +			-format STRING \ +			-type STRING \ +			-- [lindex $dat 0] +	} +} + +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 [git_read $diffcmd]} err]} { +			$status_operation 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 \ +			-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 +	} elseif {$tooltip_timer eq {}} { +		set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]] +	} +} + +method _open_tooltip {cur_w} { +	set tooltip_timer {} +	set pos_x [winfo pointerx $cur_w] +	set pos_y [winfo pointery $cur_w] +	if {[winfo containing $pos_x $pos_y] ne $cur_w} { +		_hide_tooltip $this +		return +	} + +	if {$tooltip_wm ne "$cur_w.tooltip"} { +		_hide_tooltip $this + +		set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1] +		catch {wm attributes $tooltip_wm -type tooltip} +		wm overrideredirect $tooltip_wm 1 +		wm transient $tooltip_wm [winfo toplevel $cur_w] +		set tooltip_t $tooltip_wm.label +		text $tooltip_t \ +			-takefocus 0 \ +			-highlightthickness 0 \ +			-relief flat \ +			-borderwidth 0 \ +			-wrap none \ +			-background lightyellow \ +			-foreground black +		$tooltip_t tag conf section_header -font font_uibold +		pack $tooltip_t +	} else { +		$tooltip_t conf -state normal +		$tooltip_t delete 0.0 end +	} + +	set pos @[join [list \ +		[expr {$pos_x - [winfo rootx $cur_w]}] \ +		[expr {$pos_y - [winfo rooty $cur_w]}]] ,] +	set lno [lindex [split [$cur_w index $pos] .] 0] +	if {$cur_w eq $w_amov} { +		set dat [lindex $amov_data $lno] +		set org {} +	} else { +		set dat [lindex $asim_data $lno] +		set org [lindex $amov_data $lno] +	} + +	if {$dat eq {}} { +		_hide_tooltip $this +		return +	} + +	set cmit [lindex $dat 0] +	set tooltip_commit [list $cmit] + +	set author_name {} +	set summary     {} +	set author_time {} +	catch {set author_name $header($cmit,author)} +	catch {set summary     $header($cmit,summary)} +	catch {set author_time [format_date $header($cmit,author-time)]} + +	$tooltip_t insert end "commit $cmit\n" +	$tooltip_t insert end "$author_name  $author_time\n" +	$tooltip_t insert end "$summary" + +	if {$org ne {} && [lindex $org 0] ne $cmit} { +		set save [$tooltip_t get 0.0 end] +		$tooltip_t delete 0.0 end + +		set cmit [lindex $org 0] +		set file [lindex $org 1] +		lappend tooltip_commit $cmit + +		set author_name {} +		set summary     {} +		set author_time {} +		catch {set author_name $header($cmit,author)} +		catch {set summary     $header($cmit,summary)} +		catch {set author_time [format_date $header($cmit,author-time)]} + +		$tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header +		$tooltip_t insert end "commit $cmit\n" +		$tooltip_t insert end "$author_name  $author_time\n" +		$tooltip_t insert end "$summary\n" + +		if {$file ne $path} { +			$tooltip_t insert end [strcat [mc "In File:"] " "] section_header +			$tooltip_t insert end "$file\n" +		} + +		$tooltip_t insert end "\n" +		$tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header +		$tooltip_t insert end $save +	} + +	$tooltip_t conf -state disabled +	_position_tooltip $this + +	# On MacOS raising a window causes it to acquire focus. +	# Tk 8.5 on MacOS seems to properly support wm transient, +	# so we can safely counter the effect there. +	if {[is_MacOSX]} { +		update +		if {$w eq {}} { +			raise . +		} else { +			raise $w +		} +	} +} + +method _position_tooltip {} { +	set max_h [lindex [split [$tooltip_t index end] .] 0] +	set max_w 0 +	for {set i 1} {$i <= $max_h} {incr i} { +		set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] +		if {$c > $max_w} {set max_w $c} +	} +	$tooltip_t conf -width $max_w -height $max_h + +	set req_w [winfo reqwidth  $tooltip_t] +	set req_h [winfo reqheight $tooltip_t] +	set pos_x [expr {[winfo pointerx .] +  5}] +	set pos_y [expr {[winfo pointery .] + 10}] + +	set g "${req_w}x${req_h}" +	if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +} +	append g $pos_x +	if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +} +	append g $pos_y + +	wm geometry $tooltip_wm $g +	if {![is_MacOSX]} { +		raise $tooltip_wm +	} +} + +method _hide_tooltip {} { +	if {$tooltip_wm ne {}} { +		destroy $tooltip_wm +		set tooltip_wm {} +		set tooltip_commit {} +	} +	if {$tooltip_timer ne {}} { +		after cancel $tooltip_timer +		set tooltip_timer {} +	} +} + +method _resize {new_height} { +	set diff [expr {$new_height - $old_height}] +	if {$diff == 0} return + +	set my [expr {[winfo height $w.file_pane] - 25}] +	set o [$w.file_pane sash coord 0] +	set ox [lindex $o 0] +	set oy [expr {[lindex $o 1] + $diff}] +	if {$oy < 0}   {set oy 0} +	if {$oy > $my} {set oy $my} +	$w.file_pane sash place 0 $ox $oy + +	set old_height $new_height +} + +method _show_finder {} { +	linebar::hide $gotoline +	searchbar::show $finder +} + +method _show_linebar {} { +	searchbar::hide $finder +	linebar::show $gotoline +} + +} diff --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl new file mode 100644 index 0000000000..97c9ec1c00 --- /dev/null +++ b/git-gui/lib/branch.tcl @@ -0,0 +1,40 @@ +# git-gui branch (create/delete) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc load_all_heads {} { +	global some_heads_tracking + +	set rh refs/heads +	set rh_len [expr {[string length $rh] + 1}] +	set all_heads [list] +	set fd [git_read [list for-each-ref --format=%(refname) $rh]] +	fconfigure $fd -encoding utf-8 +	while {[gets $fd line] > 0} { +		if {!$some_heads_tracking || ![is_tracking_branch $line]} { +			lappend all_heads [string range $line $rh_len end] +		} +	} +	close $fd + +	return [lsort $all_heads] +} + +proc load_all_tags {} { +	set all_tags [list] +	set fd [git_read [list for-each-ref \ +		--sort=-taggerdate \ +		--format=%(refname) \ +		refs/tags]] +	fconfigure $fd -encoding utf-8 +	while {[gets $fd line] > 0} { +		if {![regsub ^refs/tags/ $line {} name]} continue +		lappend all_tags $name +	} +	close $fd +	return $all_tags +} + +proc radio_selector {varname value args} { +	upvar #0 $varname var +	set var $value +} diff --git a/git-gui/lib/branch_checkout.tcl b/git-gui/lib/branch_checkout.tcl new file mode 100644 index 0000000000..1e6b757b35 --- /dev/null +++ b/git-gui/lib/branch_checkout.tcl @@ -0,0 +1,92 @@ +# git-gui branch checkout support +# Copyright (C) 2007 Shawn Pearce + +class branch_checkout { + +field w              ; # widget path +field w_rev          ; # mega-widget to pick the initial revision + +field opt_fetch     1; # refetch tracking branch if used? +field opt_detach    0; # force a detached head case? + +constructor dialog {} { +	make_dialog top w +	wm withdraw $w +	wm title $top [mc "%s (%s): Checkout Branch" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	ttk::label $w.header -text [mc "Checkout Branch"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.create -text [mc Checkout] \ +		-default active \ +		-command [cb _checkout] +	pack $w.buttons.create -side right +	ttk::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 + +	set w_rev [::choose_rev::new $w.rev [mc Revision]] +	$w_rev bind_listbox <Double-Button-1> [cb _checkout] +	pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + +	ttk::labelframe $w.options -text [mc Options] + +	ttk::checkbutton $w.options.fetch \ +		-text [mc "Fetch Tracking Branch"] \ +		-variable @opt_fetch +	pack $w.options.fetch -anchor nw + +	ttk::checkbutton $w.options.detach \ +		-text [mc "Detach From Local Branch"] \ +		-variable @opt_detach +	pack $w.options.detach -anchor nw + +	pack $w.options -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 _checkout]\;break +	wm deiconify $w +	tkwait window $w +} + +method _checkout {} { +	set spec [$w_rev get_tracking_branch] +	if {$spec ne {} && $opt_fetch} { +		set new {} +	} elseif {[catch {set new [$w_rev commit_or_die]}]} { +		return +	} + +	if {$opt_detach} { +		set ref {} +	} else { +		set ref [$w_rev get_local_branch] +	} + +	set co [::checkout_op::new [$w_rev get] $new $ref] +	$co parent $w +	$co enable_checkout 1 +	if {$spec ne {} && $opt_fetch} { +		$co enable_fetch $spec +	} + +	if {[$co run]} { +		destroy $w +	} else { +		$w_rev focus_filter +	} +} + +method _visible {} { +	grab $w +	$w_rev focus_filter +} + +} diff --git a/git-gui/lib/branch_create.tcl b/git-gui/lib/branch_create.tcl new file mode 100644 index 0000000000..9fded28b5c --- /dev/null +++ b/git-gui/lib/branch_create.tcl @@ -0,0 +1,222 @@ +# git-gui branch create support +# Copyright (C) 2006, 2007 Shawn Pearce + +class branch_create { + +field w              ; # widget path +field w_rev          ; # mega-widget to pick the initial revision +field w_name         ; # new branch name widget + +field name         {}; # name of the branch the user has chosen +field name_type  user; # type of branch name to use + +field opt_merge    ff; # type of merge to apply to existing branch +field opt_checkout  1; # automatically checkout the new branch? +field opt_fetch     1; # refetch tracking branch if used? +field reset_ok      0; # did the user agree to reset? + +constructor dialog {} { +	global repo_config + +	make_dialog top w +	wm withdraw $w +	wm title $top [mc "%s (%s): Create Branch" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	ttk::label $w.header -text [mc "Create New Branch"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.create -text [mc Create] \ +		-default active \ +		-command [cb _create] +	pack $w.buttons.create -side right +	ttk::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 + +	ttk::labelframe $w.desc -text [mc "Branch Name"] +	ttk::radiobutton $w.desc.name_r \ +		-text [mc "Name:"] \ +		-value user \ +		-variable @name_type +	set w_name $w.desc.name_t +	ttk::entry $w_name \ +		-width 40 \ +		-textvariable @name \ +		-validate key \ +		-validatecommand [cb _validate %d %S] +	grid $w.desc.name_r $w_name -sticky we -padx {0 5} + +	ttk::radiobutton $w.desc.match_r \ +		-text [mc "Match Tracking Branch Name"] \ +		-value match \ +		-variable @name_type +	grid $w.desc.match_r -sticky we -padx {0 5} -columnspan 2 + +	grid columnconfigure $w.desc 1 -weight 1 +	pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + +	set w_rev [::choose_rev::new $w.rev [mc "Starting Revision"]] +	pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + +	ttk::labelframe $w.options -text [mc Options] + +	ttk::frame $w.options.merge +	ttk::label $w.options.merge.l -text [mc "Update Existing Branch:"] +	pack $w.options.merge.l -side left +	ttk::radiobutton $w.options.merge.no \ +		-text [mc No] \ +		-value none \ +		-variable @opt_merge +	pack $w.options.merge.no -side left +	ttk::radiobutton $w.options.merge.ff \ +		-text [mc "Fast Forward Only"] \ +		-value ff \ +		-variable @opt_merge +	pack $w.options.merge.ff -side left +	ttk::radiobutton $w.options.merge.reset \ +		-text [mc Reset] \ +		-value reset \ +		-variable @opt_merge +	pack $w.options.merge.reset -side left +	pack $w.options.merge -anchor nw + +	ttk::checkbutton $w.options.fetch \ +		-text [mc "Fetch Tracking Branch"] \ +		-variable @opt_fetch +	pack $w.options.fetch -anchor nw + +	ttk::checkbutton $w.options.checkout \ +		-text [mc "Checkout After Creation"] \ +		-variable @opt_checkout +	pack $w.options.checkout -anchor nw +	pack $w.options -anchor nw -fill x -pady 5 -padx 5 + +	trace add variable @name_type write [cb _select] + +	set name $repo_config(gui.newbranchtemplate) +	if {[is_config_true gui.matchtrackingbranch]} { +		set name_type match +	} + +	bind $w <Visibility> [cb _visible] +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Key-Return> [cb _create]\;break +	wm deiconify $w +	tkwait window $w +} + +method _create {} { +	global repo_config +	global M1B + +	set spec [$w_rev get_tracking_branch] +	switch -- $name_type { +	user { +		set newbranch $name +	} +	match { +		if {$spec eq {}} { +			tk_messageBox \ +				-icon error \ +				-type ok \ +				-title [wm title $w] \ +				-parent $w \ +				-message [mc "Please select a tracking branch."] +			return +		} +		if {![regsub ^refs/heads/ [lindex $spec 2] {} newbranch]} { +			tk_messageBox \ +				-icon error \ +				-type ok \ +				-title [wm title $w] \ +				-parent $w \ +				-message [mc "Tracking branch %s is not a branch in the remote repository." [$w get]] +			return +		} +	} +	} + +	if {$newbranch eq {} +		|| $newbranch eq $repo_config(gui.newbranchtemplate)} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Please supply a branch name."] +		focus $w_name +		return +	} + +	if {[catch {git check-ref-format "heads/$newbranch"}]} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "'%s' is not an acceptable branch name." $newbranch] +		focus $w_name +		return +	} + +	if {$spec ne {} && $opt_fetch} { +		set new {} +	} elseif {[catch {set new [$w_rev commit_or_die]}]} { +		return +	} + +	set co [::checkout_op::new \ +		[$w_rev get] \ +		$new \ +		refs/heads/$newbranch] +	$co parent $w +	$co enable_create   1 +	$co enable_merge    $opt_merge +	$co enable_checkout $opt_checkout +	if {$spec ne {} && $opt_fetch} { +		$co enable_fetch $spec +	} +	if {$spec ne {}} { +		$co remote_source $spec +	} + +	if {[$co run]} { +		destroy $w +	} else { +		focus $w_name +	} +} + +method _validate {d S} { +	if {$d == 1} { +		if {[regexp {[~^:?*\[\0- ]} $S]} { +			return 0 +		} +		if {[string length $S] > 0} { +			set name_type user +		} +	} +	return 1 +} + +method _select {args} { +	if {$name_type eq {match}} { +		$w_rev pick_tracking_branch +	} +} + +method _visible {} { +	grab $w +	if {$name_type eq {user}} { +		$w_name icursor end +		focus $w_name +	} +} + +} diff --git a/git-gui/lib/branch_delete.tcl b/git-gui/lib/branch_delete.tcl new file mode 100644 index 0000000000..deac74a644 --- /dev/null +++ b/git-gui/lib/branch_delete.tcl @@ -0,0 +1,147 @@ +# git-gui branch delete support +# Copyright (C) 2007 Shawn Pearce + +class branch_delete { + +field w               ; # widget path +field w_heads         ; # listbox of local head names +field w_check         ; # revision picker for merge test +field w_delete        ; # delete button + +constructor dialog {} { +	global current_branch + +	make_dialog top w +	wm withdraw $w +	wm title $top [mc "%s (%s): Delete Branch" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	ttk::label $w.header -text [mc "Delete Local Branch"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	set w_delete $w.buttons.delete +	ttk::button $w_delete \ +		-text [mc Delete] \ +		-default active \ +		-state disabled \ +		-command [cb _delete] +	pack $w_delete -side right +	ttk::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 + +	ttk::labelframe $w.list -text [mc "Local Branches"] +	set w_heads $w.list.l +	slistbox $w_heads \ +		-height 10 \ +		-width 70 \ +		-selectmode extended \ +		-exportselection false +	pack $w.list.l -side left -fill both -expand 1 +	pack $w.list -fill both -expand 1 -pady 5 -padx 5 + +	set w_check [choose_rev::new \ +		$w.check \ +		[mc "Delete Only If Merged Into"] \ +		] +	$w_check none [mc "Always (Do not perform merge checks)"] +	pack $w.check -anchor nw -fill x -pady 5 -padx 5 + +	foreach h [load_all_heads] { +		if {$h ne $current_branch} { +			$w_heads insert end $h +		} +	} + +	bind $w_heads <<ListboxSelect>> [cb _select] +	bind $w <Visibility> " +		grab $w +		focus $w +	" +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Key-Return> [cb _delete]\;break +	wm deiconify $w +	tkwait window $w +} + +method _select {} { +	if {[$w_heads curselection] eq {}} { +		$w_delete configure -state disabled +	} else { +		$w_delete configure -state normal +	} +} + +method _delete {} { +	if {[catch {set check_cmt [$w_check commit_or_die]}]} { +		return +	} + +	set to_delete [list] +	set not_merged [list] +	foreach i [$w_heads curselection] { +		set b [$w_heads get $i] +		if {[catch { +			set o [git rev-parse --verify "refs/heads/$b"] +		}]} continue +		if {$check_cmt ne {}} { +			if {[catch {set m [git merge-base $o $check_cmt]}]} continue +			if {$o ne $m} { +				lappend not_merged $b +				continue +			} +		} +		lappend to_delete [list $b $o] +	} +	if {$not_merged ne {}} { +		set msg "[mc "The following branches are not completely merged into %s:" [$w_check get]] + + - [join $not_merged "\n - "]" +		tk_messageBox \ +			-icon info \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message $msg +	} +	if {$to_delete eq {}} return +	if {$check_cmt eq {}} { +		set msg [mc "Recovering deleted branches is difficult.\n\nDelete the selected branches?"] +		if {[tk_messageBox \ +			-icon warning \ +			-type yesno \ +			-title [wm title $w] \ +			-parent $w \ +			-message $msg] ne yes} { +			return +		} +	} + +	set failed {} +	foreach i $to_delete { +		set b [lindex $i 0] +		set o [lindex $i 1] +		if {[catch {git branch -D $b} err]} { +			append failed [mc " - %s:" $b] " $err\n" +		} +	} + +	if {$failed ne {}} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Failed to delete branches:\n%s" $failed] +	} + +	destroy $w +} + +} diff --git a/git-gui/lib/branch_rename.tcl b/git-gui/lib/branch_rename.tcl new file mode 100644 index 0000000000..7a3b39d6a3 --- /dev/null +++ b/git-gui/lib/branch_rename.tcl @@ -0,0 +1,130 @@ +# git-gui branch rename support +# Copyright (C) 2007 Shawn Pearce + +class branch_rename { + +field w +field oldname +field newname + +constructor dialog {} { +	global current_branch + +	make_dialog top w +	wm withdraw $w +	wm title $top [mc "%s (%s): Rename Branch" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	set oldname $current_branch +	set newname [get_config gui.newbranchtemplate] + +	ttk::label $w.header -text [mc "Rename Branch"]\ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.rename -text [mc Rename] \ +		-default active \ +		-command [cb _rename] +	pack $w.buttons.rename -side right +	ttk::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 + +	ttk::frame $w.rename +	ttk::label $w.rename.oldname_l -text [mc "Branch:"] +	ttk::combobox $w.rename.oldname_m -textvariable @oldname \ +		-values [load_all_heads] -state readonly + +	ttk::label $w.rename.newname_l -text [mc "New Name:"] +	ttk::entry $w.rename.newname_t \ +		-width 40 \ +		-textvariable @newname \ +		-validate key \ +		-validatecommand { +			if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} +			return 1 +		} + +	grid $w.rename.oldname_l $w.rename.oldname_m -sticky we -padx {0 5} +	grid $w.rename.newname_l $w.rename.newname_t -sticky we -padx {0 5} +	grid columnconfigure $w.rename 1 -weight 1 +	pack $w.rename -anchor nw -fill x -pady 5 -padx 5 + +	bind $w <Key-Return> [cb _rename] +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Visibility> " +		grab $w +		$w.rename.newname_t icursor end +		focus $w.rename.newname_t +	" +	wm deiconify $w +	tkwait window $w +} + +method _rename {} { +	global current_branch + +	if {$oldname eq {}} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Please select a branch to rename."] +		focus $w.rename.oldname_m +		return +	} +	if {$newname eq {} +		|| $newname eq [get_config gui.newbranchtemplate]} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Please supply a branch name."] +		focus $w.rename.newname_t +		return +	} +	if {![catch {git show-ref --verify -- "refs/heads/$newname"}]} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Branch '%s' already exists." $newname] +		focus $w.rename.newname_t +		return +	} +	if {[catch {git check-ref-format "heads/$newname"}]} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "'%s' is not an acceptable branch name." $newname] +		focus $w.rename.newname_t +		return +	} + +	if {[catch {git branch -m $oldname $newname} err]} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [strcat [mc "Failed to rename '%s'." $oldname] "\n\n$err"] +		return +	} + +	if {$current_branch eq $oldname} { +		set current_branch $newname +	} + +	destroy $w +} + +} diff --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl new file mode 100644 index 0000000000..fe72de025e --- /dev/null +++ b/git-gui/lib/browser.tcl @@ -0,0 +1,319 @@ +# git-gui tree browser +# Copyright (C) 2006, 2007 Shawn Pearce + +class browser { + +image create photo ::browser::img_parent  -data {R0lGODlhEAAQAIUAAPwCBBxSHBxOHMTSzNzu3KzCtBRGHCSKFIzCjLzSxBQ2FAxGHDzCLCyeHBQ+FHSmfAwuFBxKLDSCNMzizISyjJzOnDSyLAw+FAQSDAQeDBxWJAwmDAQOBKzWrDymNAQaDAQODAwaDDyKTFSyXFTGTEy6TAQCBAQKDAwiFBQyHAwSFAwmHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAZ1QIBwSCwaj0hiQCBICpcDQsFgGAaIguhhi0gohIsrQEDYMhiNrRfgeAQC5fMCAolIDhD2hFI5WC4YRBkaBxsOE2l/RxsHHA4dHmkfRyAbIQ4iIyQlB5NFGCAACiakpSZEJyinTgAcKSesACorgU4mJ6uxR35BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_rblob   -data {R0lGODlhEAAQAIUAAPwCBFxaXNze3Ly2rJSWjPz+/Ozq7GxqbJyanPT29HRydMzOzDQyNIyKjERCROTi3Pz69PTy7Pzy7PTu5Ozm3LyqlJyWlJSSjJSOhOzi1LyulPz27PTq3PTm1OzezLyqjIyKhJSKfOzaxPz29OzizLyidIyGdIyCdOTOpLymhOzavOTStMTCtMS+rMS6pMSynMSulLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaQQIAQECgajcNkQMBkDgKEQFK4LFgLhkMBIVUKroWEYlEgMLxbBKLQUBwc52HgAQ4LBo049atWQyIPA3pEdFcQEhMUFYNVagQWFxgZGoxfYRsTHB0eH5UJCJAYICEinUoPIxIcHCQkIiIllQYEGCEhJicoKYwPmiQeKisrKLFKLCwtLi8wHyUlMYwM0tPUDH5BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_xblob   -data {R0lGODlhEAAQAIYAAPwCBFRWVFxaXNza3OTi3Nze3Ly2tJyanPz+/Ozq7GxubNzSxMzOzMTGxHRybDQyNLy+vHRydHx6fKSipISChIyKjGxqbERCRCwuLLy6vGRiZExKTCQiJAwKDLSytLy2rJSSlHx+fDw6PKyqrBQWFPTu5Ozm3LyulLS2tCQmJAQCBPTq3Ozi1MSynCwqLAQGBOTazOzizOzezLyqjBweHNzSvOzaxKyurHRuZNzOtLymhDw+PIyCdOzWvOTOpLyidNzKtOTStLyifMTCtMS+rLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfZgACCAAEChYeGg4oCAwQFjgYBBwGKggEECJkICQoIkwADCwwNDY2mDA4Lng8QDhESsLARExQVDhYXGBkWExIaGw8cHR4SCQQfFQ8eFgUgIQEiwiMSBMYfGB4atwEXDyQd0wQlJicPKAHoFyIpJCoeDgMrLC0YKBsX6i4kL+4OMDEyZijr5oLGNxUqUCioEcPGDAwjPNyI6MEDChQjcOSwsUDHgw07RIgI4KCkAgs8cvTw8eOBogAxQtXIASTISiEuBwUYMoRIixYnZggpUgTDywdIkWJIitRPIAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} +image create photo ::browser::img_tree    -data {R0lGODlhEAAQAIYAAPwCBAQCBExKTBwWHMzKzOzq7ERCRExGTCwqLARqnAQ+ZHR2dKyqrNTOzHx2fCQiJMTi9NTu9HzC3AxmnAQ+XPTm7Dy67DymzITC3IzG5AxypHRydKymrMzOzOzu7BweHByy9AyGtFyy1IzG3NTu/ARupFRSVByazBR6rAyGvFyuzJTK3MTm9BR+tAxWhHS61MTi7Pz+/IymvCxulBRelAx2rHS63Pz6/PTy9PTu9Nza3ISitBRupFSixNTS1CxqnDQyNMzGzOTi5MTCxMTGxGxubGxqbLy2vLSutGRiZLy6vLSytKyurDQuNFxaXKSipDw6PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfDgACCAAECg4eIAAMEBQYHCImDBgkKCwwNBQIBBw4Bhw8QERITFJYEFQUFnoIPFhcYoRkaFBscHR4Ggh8gIRciEiMQJBkltCa6JyUoKSkXKhIrLCQYuQAPLS4TEyUhKb0qLzDVAjEFMjMuNBMoNcw21QY3ODkFOjs82RM1PfDzFRU3fOggcM7Fj2pAgggRokOHDx9DhhAZUqQaISBGhjwMEvEIkiIHEgUAkgSJkiNLmFSMJChAEydPGBSBwvJQgAc0/QQCACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_symlink -data {R0lGODlhEAAQAIQAAPwCBCwqLLSytLy+vERGRFRWVDQ2NKSmpAQCBKyurMTGxISChJyanHR2dIyKjGxubHRydGRmZIyOjFxeXHx6fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAVbICACwWieY1CibCCsrBkMb0zchSEcNYskCtqBBzshFkOGQFk0IRqOxqPBODRHCMhCQKteRc9FI/KQWGOIyFYgkDC+gPR4snCcfRGKOIKIgSMQE31+f4OEYCZ+IQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} +image create photo ::browser::img_unknown -data {R0lGODlhEAAQAIUAAPwCBFxaXIyKjNTW1Nze3LS2tJyanER2RGS+VPz+/PTu5GxqbPz69BQ6BCxeLFSqRPT29HRydMzOzDQyNERmPKSypCRWHIyKhERCRDyGPKz2nESiLBxGHCyCHGxubPz6/PTy7Ozi1Ly2rKSipOzm3LyqlKSWhCRyFOzizLymhNTKtNzOvOzaxOTStPz27OzWvOTOpLSupLyedMS+rMS6pMSulLyqjLymfLyifAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAamQIAQECgajcOkYEBoDgoBQyAJOCCuiENCsWBIh9aGw9F4HCARiXciRDQoBUnlYRlcIgsMG5CxXAgMGhscBRAEBRd7AB0eBBoIgxUfICEiikSPgyMMIAokJZcBkBybJgomIaBJAZoMpyCmqkMBFCcVCrgKKAwpoSorKqchKCwtvasIFBIhLiYvLzDHsxQNMcMKLDAwMqEz3jQ1NTY3ONyrE+jp6hN+QQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +field w +field browser_commit +field browser_path +field browser_files  {} +field browser_status [mc "Starting..."] +field browser_stack  {} +field browser_busy   1 + +field ls_buf     {}; # Buffered record output from ls-tree + +constructor new {commit {path {}}} { +	global cursor_ptr M1B +	make_dialog top w +	wm withdraw $top +	wm title $top [mc "%s (%s): File Browser" [appname] [reponame]] + +	if {$path ne {}} { +		if {[string index $path end] ne {/}} { +			append path / +		} +	} + +	set browser_commit $commit +	set browser_path "$browser_commit:[escape_path $path]" + +	ttk::label $w.path \ +		-textvariable @browser_path \ +		-anchor w \ +		-justify left \ +		-font font_uibold +	pack $w.path -anchor w -side top -fill x + +	ttk::frame $w.list +	set w_list $w.list.l +	text $w_list -background white -foreground black \ +		-borderwidth 0 \ +		-cursor $cursor_ptr \ +		-state disabled \ +		-wrap none \ +		-height 20 \ +		-width 70 \ +		-xscrollcommand [list $w.list.sbx set] \ +		-yscrollcommand [list $w.list.sby set] +	rmsel_tag $w_list +	ttk::scrollbar $w.list.sbx -orient h -command [list $w_list xview] +	ttk::scrollbar $w.list.sby -orient v -command [list $w_list yview] +	pack $w.list.sbx -side bottom -fill x +	pack $w.list.sby -side right -fill y +	pack $w_list -side left -fill both -expand 1 +	pack $w.list -side top -fill both -expand 1 + +	ttk::label $w.status \ +		-textvariable @browser_status \ +		-anchor w \ +		-justify left +	pack $w.status -anchor w -side bottom -fill x + +	bind $w_list <Button-1>        "[cb _click 0 @%x,%y];break" +	bind $w_list <Double-Button-1> "[cb _click 1 @%x,%y];break" +	bind $w_list <$M1B-Up>         "[cb _parent]        ;break" +	bind $w_list <$M1B-Left>       "[cb _parent]        ;break" +	bind $w_list <Up>              "[cb _move -1]       ;break" +	bind $w_list <Down>            "[cb _move  1]       ;break" +	bind $w_list <$M1B-Right>      "[cb _enter]         ;break" +	bind $w_list <Return>          "[cb _enter]         ;break" +	bind $w_list <Prior>           "[cb _page -1]       ;break" +	bind $w_list <Next>            "[cb _page  1]       ;break" +	bind $w_list <Left>            break +	bind $w_list <Right>           break + +	bind $w_list <Visibility> [list focus $w_list] +	wm deiconify $top +	set w $w_list +	if {$path ne {}} { +		_ls $this $browser_commit:$path $path +	} else { +		_ls $this $browser_commit $path +	} +	return $this +} + +method _move {dir} { +	if {$browser_busy} return +	set lno [lindex [split [$w index in_sel.first] .] 0] +	incr lno $dir +	if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { +		$w tag remove in_sel 0.0 end +		$w tag add in_sel $lno.0 [expr {$lno + 1}].0 +		$w see $lno.0 +	} +} + +method _page {dir} { +	if {$browser_busy} return +	$w yview scroll $dir pages +	set lno [expr {int( +		  [lindex [$w yview] 0] +		* [llength $browser_files] +		+ 1)}] +	if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { +		$w tag remove in_sel 0.0 end +		$w tag add in_sel $lno.0 [expr {$lno + 1}].0 +		$w see $lno.0 +	} +} + +method _parent {} { +	if {$browser_busy} return +	set info [lindex $browser_files 0] +	if {[lindex $info 0] eq {parent}} { +		set parent [lindex $browser_stack end-1] +		set browser_stack [lrange $browser_stack 0 end-2] +		if {$browser_stack eq {}} { +			regsub {:.*$} $browser_path {:} browser_path +		} else { +			regsub {/[^/]+/$} $browser_path {/} browser_path +		} +		set browser_status [mc "Loading %s..." $browser_path] +		_ls $this [lindex $parent 0] [lindex $parent 1] +	} +} + +method _enter {} { +	if {$browser_busy} return +	set lno [lindex [split [$w index in_sel.first] .] 0] +	set info [lindex $browser_files [expr {$lno - 1}]] +	if {$info ne {}} { +		switch -- [lindex $info 0] { +		parent { +			_parent $this +		} +		tree { +			set name [lindex $info 2] +			set escn [escape_path $name] +			set browser_status [mc "Loading %s..." $escn] +			append browser_path $escn +			_ls $this [lindex $info 1] $name +		} +		blob { +			set name [lindex $info 2] +			set p {} +			foreach n $browser_stack { +				append p [lindex $n 1] +			} +			append p $name +			blame::new $browser_commit $p {} +		} +		} +	} +} + +method _click {was_double_click pos} { +	if {$browser_busy} return +	set lno [lindex [split [$w index $pos] .] 0] +	focus $w + +	if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { +		$w tag remove in_sel 0.0 end +		$w tag add in_sel $lno.0 [expr {$lno + 1}].0 +		if {$was_double_click} { +			_enter $this +		} +	} +} + +method _ls {tree_id {name {}}} { +	set ls_buf {} +	set browser_files {} +	set browser_busy 1 + +	$w conf -state normal +	$w tag remove in_sel 0.0 end +	$w delete 0.0 end +	if {$browser_stack ne {}} { +		$w image create end \ +			-align center -padx 5 -pady 1 \ +			-name icon0 \ +			-image ::browser::img_parent +		$w insert end [mc "\[Up To Parent\]"] +		lappend browser_files parent +	} +	lappend browser_stack [list $tree_id $name] +	$w conf -state disabled + +	set fd [git_read [list ls-tree -z $tree_id]] +	fconfigure $fd -blocking 0 -encoding utf-8 +	fileevent $fd readable [cb _read $fd] +} + +method _read {fd} { +	append ls_buf [read $fd] +	set pck [split $ls_buf "\0"] +	set ls_buf [lindex $pck end] + +	set n [llength $browser_files] +	$w conf -state normal +	foreach p [lrange $pck 0 end-1] { +		set tab [string first "\t" $p] +		if {$tab == -1} continue + +		set info [split [string range $p 0 [expr {$tab - 1}]] { }] +		set path [string range $p [expr {$tab + 1}] end] +		set type   [lindex $info 1] +		set object [lindex $info 2] + +		switch -- $type { +		blob { +			scan [lindex $info 0] %o mode +			if {$mode == 0120000} { +				set image ::browser::img_symlink +			} elseif {($mode & 0100) != 0} { +				set image ::browser::img_xblob +			} else { +				set image ::browser::img_rblob +			} +		} +		tree { +			set image ::browser::img_tree +			append path / +		} +		default { +			set image ::browser::img_unknown +		} +		} + +		if {$n > 0} {$w insert end "\n"} +		$w image create end \ +			-align center -padx 5 -pady 1 \ +			-name icon[incr n] \ +			-image $image +		$w insert end [escape_path $path] +		lappend browser_files [list $type $object $path] +	} +	$w conf -state disabled + +	if {[eof $fd]} { +		close $fd +		set browser_status [mc "Ready."] +		set browser_busy 0 +		set ls_buf {} +		if {$n > 0} { +			$w tag add in_sel 1.0 2.0 +			focus -force $w +		} +	} +} ifdeleted { +	catch {close $fd} +} + +} + +class browser_open { + +field w              ; # widget path +field w_rev          ; # mega-widget to pick the initial revision + +constructor dialog {} { +	make_dialog top w +	wm withdraw $top +	wm title $top [mc "%s (%s): Browse Branch Files" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +		wm transient $top . +	} + +	ttk::label $w.header \ +		-text [mc "Browse Branch Files"] \ +		-font font_uibold \ +		-anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.browse -text [mc Browse] \ +		-default active \ +		-command [cb _open] +	pack $w.buttons.browse -side right +	ttk::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 + +	set w_rev [::choose_rev::new $w.rev [mc Revision]] +	$w_rev bind_listbox <Double-Button-1> [cb _open] +	pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + +	bind $w <Visibility> [cb _visible] +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Key-Return> [cb _open]\;break +	wm deiconify $top +	tkwait window $w +} + +method _open {} { +	if {[catch {$w_rev commit_or_die} err]} { +		return +	} +	set name [$w_rev get] +	destroy $w +	browser::new $name +} + +method _visible {} { +	grab $w +	$w_rev focus_filter +} + +} diff --git a/git-gui/lib/checkout_op.tcl b/git-gui/lib/checkout_op.tcl new file mode 100644 index 0000000000..449e89e2bc --- /dev/null +++ b/git-gui/lib/checkout_op.tcl @@ -0,0 +1,637 @@ +# git-gui commit checkout support +# Copyright (C) 2007 Shawn Pearce + +class checkout_op { + +field w        {}; # our window (if we have one) +field w_cons   {}; # embedded console window object + +field new_expr   ; # expression the user saw/thinks this is +field new_hash   ; # commit SHA-1 we are switching to +field new_ref    ; # ref we are updating/creating +field old_hash   ; # commit SHA-1 that was checked out when we started + +field parent_w      .; # window that started us +field merge_type none; # type of merge to apply to existing branch +field merge_base   {}; # merge base if we have another ref involved +field fetch_spec   {}; # refetch tracking branch if used? +field checkout      1; # actually checkout the branch? +field create        0; # create the branch if it doesn't exist? +field remote_source {}; # same as fetch_spec, to setup tracking + +field reset_ok      0; # did the user agree to reset? +field fetch_ok      0; # did the fetch succeed? + +field readtree_d   {}; # buffered output from read-tree +field update_old   {}; # was the update-ref call deferred? +field reflog_msg   {}; # log message for the update-ref call + +constructor new {expr hash {ref {}}} { +	set new_expr $expr +	set new_hash $hash +	set new_ref  $ref + +	return $this +} + +method parent {path} { +	set parent_w [winfo toplevel $path] +} + +method enable_merge {type} { +	set merge_type $type +} + +method enable_fetch {spec} { +	set fetch_spec $spec +} + +method remote_source {spec} { +	set remote_source $spec +} + +method enable_checkout {co} { +	set checkout $co +} + +method enable_create {co} { +	set create $co +} + +method run {} { +	if {$fetch_spec ne {}} { +		global M1B + +		# We were asked to refresh a single tracking branch +		# before we get to work.  We should do that before we +		# consider any ref updating. +		# +		set fetch_ok 0 +		set l_trck [lindex $fetch_spec 0] +		set remote [lindex $fetch_spec 1] +		set r_head [lindex $fetch_spec 2] +		regsub ^refs/heads/ $r_head {} r_name + +		set cmd [list git fetch $remote] +		if {$l_trck ne {}} { +			lappend cmd +$r_head:$l_trck +		} else { +			lappend cmd $r_head +		} + +		_toplevel $this {Refreshing Tracking Branch} +		set w_cons [::console::embed \ +			$w.console \ +			[mc "Fetching %s from %s" $r_name $remote]] +		pack $w.console -fill both -expand 1 +		$w_cons exec $cmd [cb _finish_fetch] + +		bind $w <$M1B-Key-w> break +		bind $w <$M1B-Key-W> break +		bind $w <Visibility> " +			[list grab $w] +			[list focus $w] +		" +		wm protocol $w WM_DELETE_WINDOW [cb _noop] +		tkwait window $w + +		if {!$fetch_ok} { +			delete_this +			return 0 +		} +	} + +	if {$new_ref ne {}} { +		# If we have a ref we need to update it before we can +		# proceed with a checkout (if one was enabled). +		# +		if {![_update_ref $this]} { +			delete_this +			return 0 +		} +	} + +	if {$checkout} { +		_checkout $this +		return 1 +	} + +	delete_this +	return 1 +} + +method _noop {} {} + +method _finish_fetch {ok} { +	if {$ok} { +		set l_trck [lindex $fetch_spec 0] +		if {$l_trck eq {}} { +			set l_trck FETCH_HEAD +		} +		if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} { +			set ok 0 +			$w_cons insert [mc "fatal: Cannot resolve %s" $l_trck] +			$w_cons insert $err +		} +	} + +	$w_cons done $ok +	set w_cons {} +	wm protocol $w WM_DELETE_WINDOW {} + +	if {$ok} { +		destroy $w +		set w {} +	} else { +		button $w.close -text [mc Close] -command [list destroy $w] +		pack $w.close -side bottom -anchor e -padx 10 -pady 10 +	} + +	set fetch_ok $ok +} + +method _update_ref {} { +	global nullid current_branch repo_config + +	set ref $new_ref +	set new $new_hash + +	set is_current 0 +	set rh refs/heads/ +	set rn [string length $rh] +	if {[string equal -length $rn $rh $ref]} { +		set newbranch [string range $ref $rn end] +		if {$current_branch eq $newbranch} { +			set is_current 1 +		} +	} else { +		set newbranch $ref +	} + +	if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} { +		# Assume it does not exist, and that is what the error was. +		# +		if {!$create} { +			_error $this [mc "Branch '%s' does not exist." $newbranch] +			return 0 +		} + +		set reflog_msg "branch: Created from $new_expr" +		set cur $nullid + +		if {($repo_config(branch.autosetupmerge) eq {true} +			|| $repo_config(branch.autosetupmerge) eq {always}) +			&& $remote_source ne {} +			&& "refs/heads/$newbranch" eq $ref} { + +			set c_remote [lindex $remote_source 1] +			set c_merge [lindex $remote_source 2] +			if {[catch { +					git config branch.$newbranch.remote $c_remote +					git config branch.$newbranch.merge  $c_merge +				} err]} { +				_error $this [strcat \ +				[mc "Failed to configure simplified git-pull for '%s'." $newbranch] \ +				"\n\n$err"] +			} +		} +	} elseif {$create && $merge_type eq {none}} { +		# We were told to create it, but not do a merge. +		# Bad.  Name shouldn't have existed. +		# +		_error $this [mc "Branch '%s' already exists." $newbranch] +		return 0 +	} elseif {!$create && $merge_type eq {none}} { +		# We aren't creating, it exists and we don't merge. +		# We are probably just a simple branch switch. +		# Use whatever value we just read. +		# +		set new      $cur +		set new_hash $cur +	} elseif {$new eq $cur} { +		# No merge would be required, don't compute anything. +		# +	} else { +		catch {set merge_base [git merge-base $new $cur]} +		if {$merge_base eq $cur} { +			# The current branch is older. +			# +			set reflog_msg "merge $new_expr: Fast-forward" +		} else { +			switch -- $merge_type { +			ff { +				if {$merge_base eq $new} { +					# The current branch is actually newer. +					# +					set new $cur +					set new_hash $cur +				} else { +					_error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr] +					return 0 +				} +			} +			reset { +				# The current branch will lose things. +				# +				if {[_confirm_reset $this $cur]} { +					set reflog_msg "reset $new_expr" +				} else { +					return 0 +				} +			} +			default { +				_error $this [mc "Merge strategy '%s' not supported." $merge_type] +				return 0 +			} +			} +		} +	} + +	if {$new ne $cur} { +		if {$is_current} { +			# No so fast.  We should defer this in case +			# we cannot update the working directory. +			# +			set update_old $cur +			return 1 +		} + +		if {[catch { +				git update-ref -m $reflog_msg $ref $new $cur +			} err]} { +			_error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"] +			return 0 +		} +	} + +	return 1 +} + +method _checkout {} { +	if {[lock_index checkout_op]} { +		after idle [cb _start_checkout] +	} else { +		_error $this [mc "Staging area (index) is already locked."] +		delete_this +	} +} + +method _start_checkout {} { +	global HEAD commit_type + +	# -- Our in memory state should match the repository. +	# +	repository_state curType old_hash curMERGE_HEAD +	if {[string match amend* $commit_type] +		&& $curType eq {normal} +		&& $old_hash eq $HEAD} { +	} elseif {$commit_type ne $curType || $HEAD ne $old_hash} { +		info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed. + +The rescan will be automatically started now. +"] +		unlock_index +		rescan ui_ready +		delete_this +		return +	} + +	if {$old_hash eq $new_hash} { +		_after_readtree $this +	} elseif {[is_config_true gui.trustmtime]} { +		_readtree $this +	} else { +		ui_status [mc "Refreshing file status..."] +		set fd [git_read [list update-index \ +			-q \ +			--unmerged \ +			--ignore-missing \ +			--refresh \ +			]] +		fconfigure $fd -blocking 0 -translation binary +		fileevent $fd readable [cb _refresh_wait $fd] +	} +} + +method _refresh_wait {fd} { +	read $fd +	if {[eof $fd]} { +		close $fd +		_readtree $this +	} +} + +method _name {} { +	if {$new_ref eq {}} { +		return [string range $new_hash 0 7] +	} + +	set rh refs/heads/ +	set rn [string length $rh] +	if {[string equal -length $rn $rh $new_ref]} { +		return [string range $new_ref $rn end] +	} else { +		return $new_ref +	} +} + +method _readtree {} { +	global HEAD + +	set readtree_d {} +	set status_bar_operation [$::main_status start \ +		[mc "Updating working directory to '%s'..." [_name $this]] \ +		[mc "files checked out"]] + +	set fd [git_read [list read-tree \ +		-m \ +		-u \ +		-v \ +		--exclude-per-directory=.gitignore \ +		$HEAD \ +		$new_hash \ +		] \ +		[list 2>@1]] +	fconfigure $fd -blocking 0 -translation binary +	fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation] +} + +method _readtree_wait {fd status_bar_operation} { +	global current_branch + +	set buf [read $fd] +	$status_bar_operation update_meter $buf +	append readtree_d $buf + +	fconfigure $fd -blocking 1 +	if {![eof $fd]} { +		fconfigure $fd -blocking 0 +		$status_bar_operation stop +		return +	} + +	if {[catch {close $fd}]} { +		set err $readtree_d +		regsub {^fatal: } $err {} err +		$status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]] +		warn_popup [strcat [mc "File level merge required."] " + +$err + +" [mc "Staying on branch '%s'." $current_branch]] +		unlock_index +		delete_this +		return +	} + +	$status_bar_operation stop +	_after_readtree $this +} + +method _after_readtree {} { +	global commit_type HEAD MERGE_HEAD PARENT +	global current_branch is_detached +	global ui_comm + +	set name [_name $this] +	set log "checkout: moving" +	if {!$is_detached} { +		append log " from $current_branch" +	} + +	# -- Move/create HEAD as a symbolic ref.  Core git does not +	#    even check for failure here, it Just Works(tm).  If it +	#    doesn't we are in some really ugly state that is difficult +	#    to recover from within git-gui. +	# +	set rh refs/heads/ +	set rn [string length $rh] +	if {[string equal -length $rn $rh $new_ref]} { +		set new_branch [string range $new_ref $rn end] +		if {$is_detached || $current_branch ne $new_branch} { +			append log " to $new_branch" +			if {[catch { +					git symbolic-ref -m $log HEAD $new_ref +				} err]} { +				_fatal $this $err +			} +			set current_branch $new_branch +			set is_detached 0 +		} +	} else { +		if {!$is_detached || $new_hash ne $HEAD} { +			append log " to $new_expr" +			if {[catch { +					_detach_HEAD $log $new_hash +				} err]} { +				_fatal $this $err +			} +		} +		set current_branch HEAD +		set is_detached 1 +	} + +	# -- We had to defer updating the branch itself until we +	#    knew the working directory would update.  So now we +	#    need to finish that work.  If it fails we're in big +	#    trouble. +	# +	if {$update_old ne {}} { +		if {[catch { +				git update-ref \ +					-m $reflog_msg \ +					$new_ref \ +					$new_hash \ +					$update_old +			} err]} { +			_fatal $this $err +		} +	} + +	if {$is_detached} { +		info_popup [mc "You are no longer on a local branch. + +If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."] +	} + +	# -- Run the post-checkout hook. +	# +	set fd_ph [githook_read post-checkout $old_hash $new_hash 1] +	if {$fd_ph ne {}} { +		global pch_error +		set pch_error {} +		fconfigure $fd_ph -blocking 0 -translation binary +		fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph] +	} else { +		_update_repo_state $this +	} +} + +method _postcheckout_wait {fd_ph} { +	global pch_error + +	append pch_error [read $fd_ph] +	fconfigure $fd_ph -blocking 1 +	if {[eof $fd_ph]} { +		if {[catch {close $fd_ph}]} { +			hook_failed_popup post-checkout $pch_error 0 +		} +		unset pch_error +		_update_repo_state $this +		return +	} +	fconfigure $fd_ph -blocking 0 +} + +method _update_repo_state {} { +	# -- Update our repository state.  If we were previously in +	#    amend mode we need to toss the current buffer and do a +	#    full rescan to update our file lists.  If we weren't in +	#    amend mode our file lists are accurate and we can avoid +	#    the rescan. +	# +	global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT +	global ui_comm + +	unlock_index +	set name [_name $this] +	set commit_type_is_amend 0 +	if {[string match amend* $commit_type]} { +		$ui_comm delete 0.0 end +		$ui_comm edit reset +		$ui_comm edit modified false +		rescan [list ui_status [mc "Checked out '%s'." $name]] +	} else { +		repository_state commit_type HEAD MERGE_HEAD +		set PARENT $HEAD +		ui_status [mc "Checked out '%s'." $name] +	} +	delete_this +} + +proc _detach_HEAD {log new} { +	git update-ref --no-deref -m $log HEAD $new +} + +method _confirm_reset {cur} { +	set reset_ok 0 +	set name [_name $this] +	set gitk [list do_gitk [list $cur ^$new_hash]] + +	_toplevel $this {Confirm Branch Reset} +	pack [label $w.msg1 \ +		-anchor w \ +		-justify left \ +		-text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\ +		] -anchor w + +	set list $w.list.l +	frame $w.list +	text $list \ +		-font font_diff \ +		-width 80 \ +		-height 10 \ +		-wrap none \ +		-xscrollcommand [list $w.list.sbx set] \ +		-yscrollcommand [list $w.list.sby set] +	scrollbar $w.list.sbx -orient h -command [list $list xview] +	scrollbar $w.list.sby -orient v -command [list $list yview] +	pack $w.list.sbx -fill x -side bottom +	pack $w.list.sby -fill y -side right +	pack $list -fill both -expand 1 +	pack $w.list -fill both -expand 1 -padx 5 -pady 5 + +	pack [label $w.msg2 \ +		-anchor w \ +		-justify left \ +		-text [mc "Recovering lost commits may not be easy."] \ +		] +	pack [label $w.msg3 \ +		-anchor w \ +		-justify left \ +		-text [mc "Reset '%s'?" $name] \ +		] + +	frame $w.buttons +	button $w.buttons.visualize \ +		-text [mc Visualize] \ +		-command $gitk +	pack $w.buttons.visualize -side left +	button $w.buttons.reset \ +		-text [mc Reset] \ +		-command " +			set @reset_ok 1 +			destroy $w +		" +	pack $w.buttons.reset -side right +	button $w.buttons.cancel \ +		-default active \ +		-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 + +	set fd [git_read [list rev-list --pretty=oneline $cur ^$new_hash]] +	while {[gets $fd line] > 0} { +		set abbr [string range $line 0 7] +		set subj [string range $line 41 end] +		$list insert end "$abbr  $subj\n" +	} +	close $fd +	$list configure -state disabled + +	bind $w    <Key-v> $gitk +	bind $w <Visibility> " +		grab $w +		focus $w.buttons.cancel +	" +	bind $w <Key-Return> [list destroy $w] +	bind $w <Key-Escape> [list destroy $w] +	tkwait window $w +	return $reset_ok +} + +method _error {msg} { +	if {[winfo ismapped $parent_w]} { +		set p $parent_w +	} else { +		set p . +	} + +	tk_messageBox \ +		-icon error \ +		-type ok \ +		-title [wm title $p] \ +		-parent $p \ +		-message $msg +} + +method _toplevel {title} { +	regsub -all {::} $this {__} w +	set w .$w + +	if {[winfo ismapped $parent_w]} { +		set p $parent_w +	} else { +		set p . +	} + +	toplevel $w +	wm title $w $title +	wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]" +} + +method _fatal {err} { +	error_popup [strcat [mc "Failed to set current branch. + +This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file. + +This should not have occurred.  %s will now close and give up." [appname]] " + +$err"] +	exit 1 +} + +} diff --git a/git-gui/lib/choose_font.tcl b/git-gui/lib/choose_font.tcl new file mode 100644 index 0000000000..a90908a8ec --- /dev/null +++ b/git-gui/lib/choose_font.tcl @@ -0,0 +1,170 @@ +# git-gui font chooser +# Copyright (C) 2007 Shawn Pearce + +class choose_font { + +field w +field w_family    ; # UI widget of all known family names +field w_example   ; # Example to showcase the chosen font + +field f_family    ; # Currently chosen family name +field f_size      ; # Currently chosen point size + +field v_family    ; # Name of global variable for family +field v_size      ; # Name of global variable for size + +variable all_families [list]  ; # All fonts known to Tk + +constructor pick {path title a_family a_size} { +	variable all_families + +	set v_family $a_family +	set v_size $a_size + +	upvar #0 $v_family pv_family +	upvar #0 $v_size pv_size + +	set f_family $pv_family +	set f_size $pv_size + +	make_dialog top w +	wm withdraw $top +	wm title $top "[appname] ([reponame]): $title" +	wm geometry $top "+[winfo rootx $path]+[winfo rooty $path]" + +	ttk::label $w.header -text $title -font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.select \ +		-text [mc Select] \ +		-default active \ +		-command [cb _select] +	ttk::button $w.buttons.cancel \ +		-text [mc Cancel] \ +		-command [list destroy $w] +	pack $w.buttons.select -side right +	pack $w.buttons.cancel -side right -padx 5 +	pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + +	ttk::frame $w.inner + +	ttk::frame $w.inner.family +	ttk::label $w.inner.family.l \ +		-text [mc "Font Family"] \ +		-anchor w +	set w_family $w.inner.family.v +	text $w_family \ +		-background white \ +		-foreground black \ +		-borderwidth 1 \ +		-relief sunken \ +		-cursor $::cursor_ptr \ +		-wrap none \ +		-width 30 \ +		-height 10 \ +		-yscrollcommand [list $w.inner.family.sby set] +	rmsel_tag $w_family +	ttk::scrollbar $w.inner.family.sby -command [list $w_family yview] +	pack $w.inner.family.l -side top -fill x +	pack $w.inner.family.sby -side right -fill y +	pack $w_family -fill both -expand 1 + +	ttk::frame $w.inner.size +	ttk::label $w.inner.size.l \ +		-text [mc "Font Size"] \ +		-anchor w +	tspinbox $w.inner.size.v \ +		-textvariable @f_size \ +		-from 2 -to 80 -increment 1 \ +		-width 3 +	bind $w.inner.size.v <FocusIn> {%W selection range 0 end} +	pack $w.inner.size.l -fill x -side top +	pack $w.inner.size.v -fill x -padx 2 + +	grid configure $w.inner.family $w.inner.size -sticky nsew +	grid rowconfigure $w.inner 0 -weight 1 +	grid columnconfigure $w.inner 0 -weight 1 +	pack $w.inner -fill both -expand 1 -padx 5 -pady 5 + +	ttk::frame $w.example +	ttk::label $w.example.l \ +		-text [mc "Font Example"] \ +		-anchor w +	set w_example $w.example.t +	text $w_example \ +		-background white \ +		-foreground black \ +		-borderwidth 1 \ +		-relief sunken \ +		-height 3 \ +		-width 40 +	rmsel_tag $w_example +	$w_example tag conf example -justify center +	$w_example insert end [mc "This is example text.\nIf you like this text, it can be your font."] example +	$w_example conf -state disabled +	pack $w.example.l -fill x +	pack $w_example -fill x +	pack $w.example -fill x -padx 5 + +	if {$all_families eq {}} { +		set all_families [lsort [font families]] +	} + +	$w_family tag conf pick +	$w_family tag bind pick <Button-1> [cb _pick_family %x %y]\;break +	foreach f $all_families { +		set sel [list pick] +		if {$f eq $f_family} { +			lappend sel in_sel +		} +		$w_family insert end "$f\n" $sel +	} +	$w_family conf -state disabled +	_update $this + +	trace add variable @f_size write [cb _update] +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Key-Return> [cb _select]\;break +	bind $w <Visibility> " +		grab $w +		focus $w +	" +	wm deiconify $w +	tkwait window $w +} + +method _select {} { +	upvar #0 $v_family pv_family +	upvar #0 $v_size pv_size + +	set pv_family $f_family +	set pv_size $f_size + +	destroy $w +} + +method _pick_family {x y} { +	variable all_families + +	set i [lindex [split [$w_family index @$x,$y] .] 0] +	set n [lindex $all_families [expr {$i - 1}]] +	if {$n ne {}} { +		$w_family tag remove in_sel 0.0 end +		$w_family tag add in_sel $i.0 [expr {$i + 1}].0 +		set f_family $n +		_update $this +	} +} + +method _update {args} { +	variable all_families + +	set i [lsearch -exact $all_families $f_family] +	if {$i < 0} return + +	$w_example tag conf example -font [list $f_family $f_size] +	$w_family see [expr {$i + 1}].0 +} + +} diff --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl new file mode 100644 index 0000000000..7e1462a20c --- /dev/null +++ b/git-gui/lib/choose_repository.tcl @@ -0,0 +1,729 @@ +# git-gui Git repository chooser +# Copyright (C) 2007 Shawn Pearce + +class choose_repository { + +field top +field w +field w_body      ; # Widget holding the center content +field w_next      ; # Next button +field w_quit      ; # Quit button +field o_cons      ; # Console object (if active) + +field w_types     ; # List of type buttons in clone +field w_recentlist ; # Listbox containing recent repositories +field w_localpath  ; # Entry widget bound to local_path + +field done              0 ; # Finished picking the repository? +field clone_ok      false ; # clone succeeeded +field local_path       {} ; # Where this repository is locally +field origin_url       {} ; # Where we are cloning from +field origin_name  origin ; # What we shall call 'origin' +field clone_type hardlink ; # Type of clone to construct +field recursive      true ; # Recursive cloning flag +field readtree_err        ; # Error output from read-tree (if any) +field sorted_recent       ; # recent repositories (sorted) + +constructor pick {} { +	global M1T M1B + +	if {[set maxrecent [get_config gui.maxrecentrepo]] eq {}} { +		set maxrecent 10 +	} + +	make_dialog top w +	wm title $top [mc "Git Gui"] + +	if {$top eq {.}} { +		menu $w.mbar -tearoff 0 +		$top configure -menu $w.mbar + +		set m_repo $w.mbar.repository +		$w.mbar add cascade \ +			-label [mc Repository] \ +			-menu $m_repo +		menu $m_repo + +		if {[is_MacOSX]} { +			$w.mbar add cascade -label Apple -menu .mbar.apple +			menu $w.mbar.apple +			$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 +		bind $top <$M1B-q> exit +		bind $top <$M1B-Q> exit +		bind $top <Key-Escape> exit +	} else { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +		bind $top <Key-Escape> [list destroy $top] +		set m_repo {} +	} + +	pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10 + +	set w_body $w.body +	set opts $w_body.options +	ttk::frame $w_body +	text $opts \ +		-cursor $::cursor_ptr \ +		-relief flat \ +		-background [get_bg_color $w_body] \ +		-wrap none \ +		-spacing1 5 \ +		-width 50 \ +		-height 3 +	pack $opts -anchor w -fill x + +	$opts tag conf link_new -foreground blue -underline 1 +	$opts tag bind link_new <1> [cb _next new] +	$opts insert end [mc "Create New Repository"] link_new +	$opts insert end "\n" +	if {$m_repo ne {}} { +		$m_repo add command \ +			-command [cb _next new] \ +			-accelerator $M1T-N \ +			-label [mc "New..."] +		bind $top <$M1B-n> [cb _next new] +		bind $top <$M1B-N> [cb _next new] +	} + +	$opts tag conf link_clone -foreground blue -underline 1 +	$opts tag bind link_clone <1> [cb _next clone] +	$opts insert end [mc "Clone Existing Repository"] link_clone +	$opts insert end "\n" +	if {$m_repo ne {}} { +		if {[tk windowingsystem] eq "win32"} { +			set key L +		} else { +			set key C +		} +		$m_repo add command \ +			-command [cb _next clone] \ +			-accelerator $M1T-$key \ +			-label [mc "Clone..."] +		bind $top <$M1B-[string tolower $key]> [cb _next clone] +		bind $top <$M1B-[string toupper $key]> [cb _next clone] +	} + +	$opts tag conf link_open -foreground blue -underline 1 +	$opts tag bind link_open <1> [cb _next open] +	$opts insert end [mc "Open Existing Repository"] link_open +	$opts insert end "\n" +	if {$m_repo ne {}} { +		$m_repo add command \ +			-command [cb _next open] \ +			-accelerator $M1T-O \ +			-label [mc "Open..."] +		bind $top <$M1B-o> [cb _next open] +		bind $top <$M1B-O> [cb _next open] +	} + +	$opts conf -state disabled + +	set sorted_recent [_get_recentrepos] +	if {[llength $sorted_recent] > 0} { +		if {$m_repo ne {}} { +			$m_repo add separator +			$m_repo add command \ +				-state disabled \ +				-label [mc "Recent Repositories"] +		} + +	if {[set lenrecent [llength $sorted_recent]] < $maxrecent} { +		set lenrecent $maxrecent +	} + +		ttk::label $w_body.space +		ttk::label $w_body.recentlabel \ +			-anchor w \ +			-text [mc "Open Recent Repository:"] +		set w_recentlist $w_body.recentlist +		text $w_recentlist \ +			-cursor $::cursor_ptr \ +			-relief flat \ +			-background [get_bg_color $w_body.recentlabel] \ +			-wrap none \ +			-width 50 \ +			-height $lenrecent +		$w_recentlist tag conf link \ +			-foreground blue \ +			-underline 1 +		set home $::env(HOME) +		set home "[file normalize $home]/" +		set hlen [string length $home] +		foreach p $sorted_recent { +			set path $p +			if {[string equal -length $hlen $home $p]} { +				set p "~/[string range $p $hlen end]" +			} +			regsub -all "\n" $p "\\n" p +			$w_recentlist insert end $p link +			$w_recentlist insert end "\n" + +			if {$m_repo ne {}} { +				$m_repo add command \ +					-command [cb _open_recent_path $path] \ +					-label "    $p" +			} +		} +		$w_recentlist conf -state disabled +		$w_recentlist tag bind link <1> [cb _open_recent %x,%y] +		pack $w_body.space -anchor w -fill x +		pack $w_body.recentlabel -anchor w -fill x +		pack $w_recentlist -anchor w -fill x +	} +	pack $w_body -fill x -padx 10 -pady 10 + +	ttk::frame $w.buttons +	set w_next $w.buttons.next +	set w_quit $w.buttons.quit +	ttk::button $w_quit \ +		-text [mc "Quit"] \ +		-command exit +	pack $w_quit -side right -padx 5 +	pack $w.buttons -side bottom -fill x -padx 10 -pady 10 + +	if {$m_repo ne {}} { +		$m_repo add separator +		$m_repo add command \ +			-label [mc Quit] \ +			-command exit \ +			-accelerator $M1T-Q +	} + +	bind $top <Return> [cb _invoke_next] +	bind $top <Visibility> " +		[cb _center] +		grab $top +		focus $top +		bind $top <Visibility> {} +	" +	wm deiconify $top +	tkwait variable @done + +	grab release $top +	if {$top eq {.}} { +		eval destroy [winfo children $top] +	} +} + +method _center {} { +	set nx [winfo reqwidth $top] +	set ny [winfo reqheight $top] +	set rx [expr {([winfo screenwidth  $top] - $nx) / 3}] +	set ry [expr {([winfo screenheight $top] - $ny) / 3}] +	wm geometry $top [format {+%d+%d} $rx $ry] +} + +method _invoke_next {} { +	if {[winfo exists $w_next]} { +		uplevel #0 [$w_next cget -command] +	} +} + +proc _get_recentrepos {} { +	set recent [list] +	foreach p [lsort -unique [get_config gui.recentrepo]] { +		if {[_is_git [file join $p .git]]} { +			lappend recent $p +		} else { +			_unset_recentrepo $p +		} +	} +	return $recent +} + +proc _unset_recentrepo {p} { +	regsub -all -- {([()\[\]{}\.^$+*?\\])} $p {\\\1} p +	catch {git config --global --unset-all gui.recentrepo "^$p\$"} +	load_config 1 +} + +proc _append_recentrepos {path} { +	set path [file normalize $path] +	set recent [get_config gui.recentrepo] + +	if {[lindex $recent end] eq $path} { +		return +	} + +	set i [lsearch $recent $path] +	if {$i >= 0} { +		_unset_recentrepo $path +	} + +	git config --global --add gui.recentrepo $path +	load_config 1 +	set recent [get_config gui.recentrepo] + +	if {[set maxrecent [get_config gui.maxrecentrepo]] eq {}} { +		set maxrecent 10 +	} + +	while {[llength $recent] > $maxrecent} { +		_unset_recentrepo [lindex $recent 0] +		set recent [get_config gui.recentrepo] +	} +} + +method _open_recent {xy} { +	set id [lindex [split [$w_recentlist index @$xy] .] 0] +	set local_path [lindex $sorted_recent [expr {$id - 1}]] +	_do_open2 $this +} + +method _open_recent_path {p} { +	set local_path $p +	_do_open2 $this +} + +method _next {action} { +	destroy $w_body +	if {![winfo exists $w_next]} { +		ttk::button $w_next -default active +		set pos -before +		if {[tk windowingsystem] eq "win32"} { set pos -after } +		pack $w_next -side right -padx 5 $pos $w_quit +	} +	_do_$action $this +} + +method _write_local_path {args} { +	if {$local_path eq {}} { +		$w_next conf -state disabled +	} else { +		$w_next conf -state normal +	} +} + +method _git_init {} { +	if {[catch {git init $local_path} err]} { +		error_popup [strcat \ +			[mc "Failed to create repository %s:" $local_path] \ +			"\n\n$err"] +		return 0 +	} + +	if {[catch {cd $local_path} err]} { +		error_popup [strcat \ +			[mc "Failed to create repository %s:" $local_path] \ +			"\n\n$err"] +		return 0 +	} + +	_append_recentrepos [pwd] +	set ::_gitdir .git +	set ::_prefix {} +	return 1 +} + +proc _is_git {path {outdir_var ""}} { +	if {$outdir_var ne ""} { +		upvar 1 $outdir_var outdir +	} +	if {[catch {set outdir [git rev-parse --resolve-git-dir $path]}]} { +		return 0 +	} +	return 1 +} + +###################################################################### +## +## Create New Repository + +method _do_new {} { +	$w_next conf \ +		-state disabled \ +		-command [cb _do_new2] \ +		-text [mc "Create"] + +	ttk::frame $w_body +	ttk::label $w_body.h \ +		-font font_uibold -anchor center \ +		-text [mc "Create New Repository"] +	pack $w_body.h -side top -fill x -pady 10 +	pack $w_body -fill x -padx 10 + +	ttk::frame $w_body.where +	ttk::label $w_body.where.l -text [mc "Directory:"] +	ttk::entry $w_body.where.t \ +		-textvariable @local_path \ +		-width 50 +	ttk::button $w_body.where.b \ +		-text [mc "Browse"] \ +		-command [cb _new_local_path] +	set w_localpath $w_body.where.t + +	grid $w_body.where.l $w_body.where.t $w_body.where.b -sticky ew +	pack $w_body.where -fill x + +	grid columnconfigure $w_body.where 1 -weight 1 + +	trace add variable @local_path write [cb _write_local_path] +	bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]] +	update +	focus $w_body.where.t +} + +method _new_local_path {} { +	if {$local_path ne {}} { +		set p [file dirname $local_path] +	} else { +		set p [pwd] +	} + +	set p [tk_chooseDirectory \ +		-initialdir $p \ +		-parent $top \ +		-title [mc "Git Repository"] \ +		-mustexist false] +	if {$p eq {}} return + +	set p [file normalize $p] +	if {![_new_ok $p]} { +		return +	} +	set local_path $p +	$w_localpath icursor end +} + +method _do_new2 {} { +	if {![_new_ok $local_path]} { +		return +	} +	if {![_git_init $this]} { +		return +	} +	set done 1 +} + +proc _new_ok {p} { +	if {[file isdirectory $p]} { +		if {[_is_git [file join $p .git]]} { +			error_popup [mc "Directory %s already exists." $p] +			return 0 +		} +	} elseif {[file exists $p]} { +		error_popup [mc "File %s already exists." $p] +		return 0 +	} +	return 1 +} + +###################################################################### +## +## Clone Existing Repository + +method _do_clone {} { +	$w_next conf \ +		-state disabled \ +		-command [cb _do_clone2] \ +		-text [mc "Clone"] + +	ttk::frame $w_body +	ttk::label $w_body.h \ +		-font font_uibold -anchor center \ +		-text [mc "Clone Existing Repository"] +	pack $w_body.h -side top -fill x -pady 10 +	pack $w_body -fill x -padx 10 + +	set args $w_body.args +	ttk::frame $w_body.args +	pack $args -fill both + +	ttk::label $args.origin_l -text [mc "Source Location:"] +	ttk::entry $args.origin_t \ +		-textvariable @origin_url \ +		-width 50 +	ttk::button $args.origin_b \ +		-text [mc "Browse"] \ +		-command [cb _open_origin] +	grid $args.origin_l $args.origin_t $args.origin_b -sticky ew + +	ttk::label $args.where_l -text [mc "Target Directory:"] +	ttk::entry $args.where_t \ +		-textvariable @local_path \ +		-width 50 +	ttk::button $args.where_b \ +		-text [mc "Browse"] \ +		-command [cb _new_local_path] +	grid $args.where_l $args.where_t $args.where_b -sticky ew +	set w_localpath $args.where_t + +	ttk::label $args.type_l -text [mc "Clone Type:"] +	ttk::frame $args.type_f +	set w_types [list] +	lappend w_types [ttk::radiobutton $args.type_f.hardlink \ +		-state disabled \ +		-text [mc "Standard (Fast, Semi-Redundant, Hardlinks)"] \ +		-variable @clone_type \ +		-value hardlink] +	lappend w_types [ttk::radiobutton $args.type_f.full \ +		-state disabled \ +		-text [mc "Full Copy (Slower, Redundant Backup)"] \ +		-variable @clone_type \ +		-value full] +	lappend w_types [ttk::radiobutton $args.type_f.shared \ +		-state disabled \ +		-text [mc "Shared (Fastest, Not Recommended, No Backup)"] \ +		-variable @clone_type \ +		-value shared] +	foreach r $w_types { +		pack $r -anchor w +	} +	ttk::checkbutton $args.type_f.recursive \ +		-text [mc "Recursively clone submodules too"] \ +		-variable @recursive \ +		-onvalue true -offvalue false +	pack $args.type_f.recursive -anchor w +	grid $args.type_l $args.type_f -sticky new + +	grid columnconfigure $args 1 -weight 1 + +	trace add variable @local_path write [cb _update_clone] +	trace add variable @origin_url write [cb _update_clone] +	bind $w_body.h <Destroy> " +		[list trace remove variable @local_path write [cb _update_clone]] +		[list trace remove variable @origin_url write [cb _update_clone]] +	" +	update +	focus $args.origin_t +} + +method _open_origin {} { +	if {$origin_url ne {} && [file isdirectory $origin_url]} { +		set p $origin_url +	} else { +		set p [pwd] +	} + +	set p [tk_chooseDirectory \ +		-initialdir $p \ +		-parent $top \ +		-title [mc "Git Repository"] \ +		-mustexist true] +	if {$p eq {}} return + +	set p [file normalize $p] +	if {![_is_git [file join $p .git]] && ![_is_git $p]} { +		error_popup [mc "Not a Git repository: %s" [file tail $p]] +		return +	} +	set origin_url $p +} + +method _update_clone {args} { +	if {$local_path ne {} && $origin_url ne {}} { +		$w_next conf -state normal +	} else { +		$w_next conf -state disabled +	} + +	if {$origin_url ne {} && +		(  [_is_git [file join $origin_url .git]] +		|| [_is_git $origin_url])} { +		set e normal +		if {[[lindex $w_types 0] cget -state] eq {disabled}} { +			set clone_type hardlink +		} +	} else { +		set e disabled +		set clone_type full +	} + +	foreach r $w_types { +		$r conf -state $e +	} +} + +method _do_clone2 {} { +	if {[file isdirectory $origin_url]} { +		set origin_url [file normalize $origin_url] +		if {$clone_type eq {hardlink}} { +			# cannot use hardlinks if this is a linked worktree (.gitfile or git-new-workdir) +			if {[git -C $origin_url rev-parse --is-inside-work-tree] == {true}} { +				set islink 0 +				set dotgit [file join $origin_url .git] +				if {[file isfile $dotgit]} { +					set islink 1 +				} else { +					set objdir [file join $dotgit objects] +					if {[file exists $objdir] && [file type $objdir] == {link}} { +						set islink 1 +					} +				} +				if {$islink} { +					info_popup [mc "Hardlinks are unavailable.  Falling back to copying."] +					set clone_type full +				} +			} +		} +	} + +	if {$clone_type eq {hardlink} && ![file isdirectory $origin_url]} { +		error_popup [mc "Standard only available for local repository."] +		return +	} +	if {$clone_type eq {shared} && ![file isdirectory $origin_url]} { +		error_popup [mc "Shared only available for local repository."] +		return +	} + +	set giturl $origin_url + +	if {[file exists $local_path]} { +		error_popup [mc "Location %s already exists." $local_path] +		return +	} + +	set clone_options {--progress} +	if {$recursive} { +		append clone_options { --recurse-submodules} +	} + +	destroy $w_body $w_next + +	switch -exact -- $clone_type { +		full { +			append clone_options { --no-hardlinks --no-local} +		} +		shared { +			append clone_options { --shared} +		} +	} + +	if {[catch { +		set o_cons [console::embed \ +			$w_body \ +			[mc "Cloning from %s" $origin_url]] +		pack $w_body -fill both -expand 1 -padx 10 +		$o_cons exec \ +			[list git clone {*}$clone_options $origin_url $local_path] \ +			[cb _do_clone2_done] +	} err]} { +		error_popup [strcat [mc "Clone failed."] "\n" $err] +		return +	} + +	tkwait variable @done +	if {!$clone_ok} { +		error_popup [mc "Clone failed."] +		return +	} +} + +method _do_clone2_done {ok} { +	$o_cons done $ok +	if {$ok} { +		if {[catch { +			cd $local_path +			set ::_gitdir .git +			set ::_prefix {} +			_append_recentrepos [pwd] +		} err]} { +			set ok 0 +		} +	} +	if {!$ok} { +		set ::_gitdir {} +		set ::_prefix {} +	} +	set clone_ok $ok +	set done 1 +} + + +###################################################################### +## +## Open Existing Repository + +method _do_open {} { +	$w_next conf \ +		-state disabled \ +		-command [cb _do_open2] \ +		-text [mc "Open"] + +	ttk::frame $w_body +	ttk::label $w_body.h \ +		-font font_uibold -anchor center \ +		-text [mc "Open Existing Repository"] +	pack $w_body.h -side top -fill x -pady 10 +	pack $w_body -fill x -padx 10 + +	ttk::frame $w_body.where +	ttk::label $w_body.where.l -text [mc "Repository:"] +	ttk::entry $w_body.where.t \ +		-textvariable @local_path \ +		-width 50 +	ttk::button $w_body.where.b \ +		-text [mc "Browse"] \ +		-command [cb _open_local_path] + +	grid $w_body.where.l $w_body.where.t $w_body.where.b -sticky ew +	pack $w_body.where -fill x + +	grid columnconfigure $w_body.where 1 -weight 1 + +	trace add variable @local_path write [cb _write_local_path] +	bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]] +	update +	focus $w_body.where.t +} + +method _open_local_path {} { +	if {$local_path ne {}} { +		set p $local_path +	} else { +		set p [pwd] +	} + +	set p [tk_chooseDirectory \ +		-initialdir $p \ +		-parent $top \ +		-title [mc "Git Repository"] \ +		-mustexist true] +	if {$p eq {}} return + +	set p [file normalize $p] +	if {![_is_git [file join $p .git]]} { +		error_popup [mc "Not a Git repository: %s" [file tail $p]] +		return +	} +	set local_path $p +} + +method _do_open2 {} { +	if {![_is_git [file join $local_path .git] actualgit]} { +		error_popup [mc "Not a Git repository: %s" [file tail $local_path]] +		return +	} + +	if {[catch {cd $local_path} err]} { +		error_popup [strcat \ +			[mc "Failed to open repository %s:" $local_path] \ +			"\n\n$err"] +		return +	} + +	_append_recentrepos [pwd] +	set ::_gitdir $actualgit +	set ::_prefix {} +	set done 1 +} + +} diff --git a/git-gui/lib/choose_rev.tcl b/git-gui/lib/choose_rev.tcl new file mode 100644 index 0000000000..cd355cc92a --- /dev/null +++ b/git-gui/lib/choose_rev.tcl @@ -0,0 +1,624 @@ +# git-gui revision chooser +# Copyright (C) 2006, 2007 Shawn Pearce + +class choose_rev { + +image create photo ::choose_rev::img_find -data {R0lGODlhEAAQAIYAAPwCBCQmJDw+PBQSFAQCBMza3NTm5MTW1HyChOT29Ozq7MTq7Kze5Kzm7Oz6/NTy9Iza5GzGzKzS1Nzy9Nz29Kzq9HTGzHTK1Lza3AwKDLzu9JTi7HTW5GTCzITO1Mzq7Hza5FTK1ESyvHzKzKzW3DQyNDyqtDw6PIzW5HzGzAT+/Dw+RKyurNTOzMTGxMS+tJSGdATCxHRydLSqpLymnLSijBweHERCRNze3Pz69PTy9Oze1OTSxOTGrMSqlLy+vPTu5OzSvMymjNTGvNS+tMy2pMyunMSefAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAe4gACCAAECA4OIiAIEBQYHBAKJgwIICQoLDA0IkZIECQ4PCxARCwSSAxITFA8VEBYXGBmJAQYLGhUbHB0eH7KIGRIMEBAgISIjJKaIJQQLFxERIialkieUGigpKRoIBCqJKyyLBwvJAioEyoICLS4v6QQwMQQyLuqLli8zNDU2BCf1lN3AkUPHDh49fAQAAEnGD1MCCALZEaSHkIUMBQS8wWMIkSJGhBzBmFEGgRsBUqpMiSgdAD+BAAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +field w               ; # our megawidget path +field w_list          ; # list of currently filtered specs +field w_filter        ; # filter entry for $w_list + +field c_expr        {}; # current revision expression +field filter        ""; # current filter string +field revtype     head; # type of revision chosen +field cur_specs [list]; # list of specs for $revtype +field spec_head       ; # list of all head specs +field spec_trck       ; # list of all tracking branch specs +field spec_tag        ; # list of all tag specs +field tip_data        ; # array of tip commit info by refname +field log_last        ; # array of reflog date by refname + +field tooltip_wm        {} ; # Current tooltip toplevel, if open +field tooltip_t         {} ; # Text widget in $tooltip_wm +field tooltip_timer     {} ; # Current timer event for our tooltip + +proc new {path {title {}}} { +	return [_new $path 0 $title] +} + +proc new_unmerged {path {title {}}} { +	return [_new $path 1 $title] +} + +constructor _new {path unmerged_only title} { +	global current_branch is_detached + +	if {![info exists ::all_remotes]} { +		load_all_remotes +	} + +	set w $path + +	if {$title ne {}} { +		ttk::labelframe $w -text $title +	} else { +		ttk::frame $w +	} +	bind $w <Destroy> [cb _delete %W] + +	if {$is_detached} { +		ttk::radiobutton $w.detachedhead_r \ +			-text [mc "This Detached Checkout"] \ +			-value HEAD \ +			-variable @revtype +		grid $w.detachedhead_r -sticky we -padx {0 5} -columnspan 2 +	} + +	ttk::radiobutton $w.expr_r \ +		-text [mc "Revision Expression:"] \ +		-value expr \ +		-variable @revtype +	ttk::entry $w.expr_t \ +		-width 50 \ +		-textvariable @c_expr \ +		-validate key \ +		-validatecommand [cb _validate %d %S] +	grid $w.expr_r $w.expr_t -sticky we -padx {0 5} + +	ttk::frame $w.types +	ttk::radiobutton $w.types.head_r \ +		-text [mc "Local Branch"] \ +		-value head \ +		-variable @revtype +	pack $w.types.head_r -side left +	ttk::radiobutton $w.types.trck_r \ +		-text [mc "Tracking Branch"] \ +		-value trck \ +		-variable @revtype +	pack $w.types.trck_r -side left +	ttk::radiobutton $w.types.tag_r \ +		-text [mc "Tag"] \ +		-value tag \ +		-variable @revtype +	pack $w.types.tag_r -side left +	set w_filter $w.types.filter +	ttk::entry $w_filter \ +		-width 12 \ +		-textvariable @filter \ +		-validate key \ +		-validatecommand [cb _filter %P] +	pack $w_filter -side right +	pack [ttk::label $w.types.filter_icon \ +		-image ::choose_rev::img_find \ +		] -side right +	grid $w.types -sticky we -padx {0 5} -columnspan 2 + +	ttk::frame $w.list -style SListbox.TFrame -padding 2 +	set w_list $w.list.l +	listbox $w_list \ +		-font font_diff \ +		-width 50 \ +		-height 10 \ +		-selectmode browse \ +		-exportselection false \ +		-xscrollcommand [cb _sb_set $w.list.sbx h] \ +		-yscrollcommand [cb _sb_set $w.list.sby v] +	$w_list configure -relief flat -highlightthickness 0 -borderwidth 0 +	pack $w_list -fill both -expand 1 +	grid $w.list -sticky nswe -padx {20 5} -columnspan 2 +	bind $w_list <Any-Motion>  [cb _show_tooltip @%x,%y] +	bind $w_list <Any-Enter>   [cb _hide_tooltip] +	bind $w_list <Any-Leave>   [cb _hide_tooltip] +	bind $w_list <Destroy>     [cb _hide_tooltip] + +	grid columnconfigure $w 1 -weight 1 +	if {$is_detached} { +		grid rowconfigure $w 3 -weight 1 +	} else { +		grid rowconfigure $w 2 -weight 1 +	} + +	trace add variable @revtype write [cb _select] +	bind $w_filter <Key-Return> [list focus $w_list]\;break +	bind $w_filter <Key-Down>   [list focus $w_list] + +	set fmt list +	append fmt { %(refname)} +	append fmt { [list} +	append fmt { %(objecttype)} +	append fmt { %(objectname)} +	append fmt { [concat %(taggername) %(authorname)]} +	append fmt { [reformat_date [concat %(taggerdate) %(authordate)]]} +	append fmt { %(subject)} +	append fmt {] [list} +	append fmt { %(*objecttype)} +	append fmt { %(*objectname)} +	append fmt { %(*authorname)} +	append fmt { [reformat_date %(*authordate)]} +	append fmt { %(*subject)} +	append fmt {]} +	set all_refn [list] +	set fr_fd [git_read [list for-each-ref \ +		--tcl \ +		--sort=-taggerdate \ +		--format=$fmt \ +		refs/heads \ +		refs/remotes \ +		refs/tags \ +		]] +	fconfigure $fr_fd -encoding utf-8 +	while {[gets $fr_fd line] > 0} { +		set line [eval $line] +		if {[lindex $line 1 0] eq {tag}} { +			if {[lindex $line 2 0] eq {commit}} { +				set sha1 [lindex $line 2 1] +			} else { +				continue +			} +		} elseif {[lindex $line 1 0] eq {commit}} { +			set sha1 [lindex $line 1 1] +		} else { +			continue +		} +		set refn [lindex $line 0] +		set tip_data($refn) [lrange $line 1 end] +		lappend cmt_refn($sha1) $refn +		lappend all_refn $refn +	} +	close $fr_fd + +	if {$unmerged_only} { +		set fr_fd [git_read [list rev-list --all ^$::HEAD]] +		while {[gets $fr_fd sha1] > 0} { +			if {[catch {set rlst $cmt_refn($sha1)}]} continue +			foreach refn $rlst { +				set inc($refn) 1 +			} +		} +		close $fr_fd +	} else { +		foreach refn $all_refn { +			set inc($refn) 1 +		} +	} + +	set spec_head [list] +	foreach name [load_all_heads] { +		set refn refs/heads/$name +		if {[info exists inc($refn)]} { +			lappend spec_head [list $name $refn] +		} +	} + +	set spec_trck [list] +	foreach spec [all_tracking_branches] { +		set refn [lindex $spec 0] +		if {[info exists inc($refn)]} { +			regsub ^refs/(heads|remotes)/ $refn {} name +			lappend spec_trck [concat $name $spec] +		} +	} + +	set spec_tag [list] +	foreach name [load_all_tags] { +		set refn refs/tags/$name +		if {[info exists inc($refn)]} { +			lappend spec_tag [list $name $refn] +		} +	} + +		  if {$is_detached}             { set revtype HEAD +	} elseif {[llength $spec_head] > 0} { set revtype head +	} elseif {[llength $spec_trck] > 0} { set revtype trck +	} elseif {[llength $spec_tag ] > 0} { set revtype tag +	} else {                              set revtype expr +	} + +	if {$revtype eq {head} && $current_branch ne {}} { +		set i 0 +		foreach spec $spec_head { +			if {[lindex $spec 0] eq $current_branch} { +				$w_list selection clear 0 end +				$w_list selection set $i +				break +			} +			incr i +		} +	} + +	return $this +} + +method none {text} { +	if {![winfo exists $w.none_r]} { +		ttk::radiobutton $w.none_r \ +			-value none \ +			-variable @revtype +		grid $w.none_r -sticky we -padx {0 5} -columnspan 2 +	} +	$w.none_r configure -text $text +} + +method get {} { +	switch -- $revtype { +	head - +	trck - +	tag  { +		set i [$w_list curselection] +		if {$i ne {}} { +			return [lindex $cur_specs $i 0] +		} else { +			return {} +		} +	} + +	HEAD { return HEAD                     } +	expr { return $c_expr                  } +	none { return {}                       } +	default { error "unknown type of revision" } +	} +} + +method pick_tracking_branch {} { +	set revtype trck +} + +method focus_filter {} { +	if {[$w_filter cget -state] eq {normal}} { +		focus $w_filter +	} +} + +method bind_listbox {event script}  { +	bind $w_list $event $script +} + +method get_local_branch {} { +	if {$revtype eq {head}} { +		return [_expr $this] +	} else { +		return {} +	} +} + +method get_tracking_branch {} { +	set i [$w_list curselection] +	if {$i eq {} || $revtype ne {trck}} { +		return {} +	} +	return [lrange [lindex $cur_specs $i] 1 end] +} + +method get_commit {} { +	set e [_expr $this] +	if {$e eq {}} { +		return {} +	} +	return [git rev-parse --verify "$e^0"] +} + +method commit_or_die {} { +	if {[catch {set new [get_commit $this]} err]} { + +		# Cleanup the not-so-friendly error from rev-parse. +		# +		regsub {^fatal:\s*} $err {} err +		if {$err eq {Needed a single revision}} { +			set err {} +		} + +		set top [winfo toplevel $w] +		set msg [strcat [mc "Invalid revision: %s" [get $this]] "\n\n$err"] +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $top] \ +			-parent $top \ +			-message $msg +		error $msg +	} +	return $new +} + +method _expr {} { +	switch -- $revtype { +	head - +	trck - +	tag  { +		set i [$w_list curselection] +		if {$i ne {}} { +			return [lindex $cur_specs $i 1] +		} else { +			error [mc "No revision selected."] +		} +	} + +	expr { +		if {$c_expr ne {}} { +			return $c_expr +		} else { +			error [mc "Revision expression is empty."] +		} +	} +	HEAD { return HEAD                     } +	none { return {}                       } +	default { error "unknown type of revision"      } +	} +} + +method _validate {d S} { +	if {$d == 1} { +		if {[regexp {\s} $S]} { +			return 0 +		} +		if {[string length $S] > 0} { +			set revtype expr +		} +	} +	return 1 +} + +method _filter {P} { +	if {[regexp {\s} $P]} { +		return 0 +	} +	_rebuild $this $P +	return 1 +} + +method _select {args} { +	_rebuild $this $filter +	focus_filter $this +} + +method _rebuild {pat} { +	set ste normal +	switch -- $revtype { +	head { set new $spec_head } +	trck { set new $spec_trck } +	tag  { set new $spec_tag  } +	expr - +	HEAD - +	none { +		set new [list] +		set ste disabled +	} +	} + +	if {[$w_list cget -state] eq {disabled}} { +		$w_list configure -state normal +	} +	$w_list delete 0 end + +	if {$pat ne {}} { +		set pat *${pat}* +	} +	set cur_specs [list] +	foreach spec $new { +		set txt [lindex $spec 0] +		if {$pat eq {} || [string match $pat $txt]} { +			lappend cur_specs $spec +			$w_list insert end $txt +		} +	} +	if {$cur_specs ne {}} { +		$w_list selection clear 0 end +		$w_list selection set 0 +	} + +	if {[$w_filter cget -state] ne $ste} { +		$w_list   configure -state $ste +		$w_filter configure -state $ste +	} +} + +method _delete {current} { +	if {$current eq $w} { +		delete_this +	} +} + +method _sb_set {sb orient first last} { +	set old_focus [focus -lastfor $w] + +	if {$first == 0 && $last == 1} { +		if {[winfo exists $sb]} { +			destroy $sb +			if {$old_focus ne {}} { +				update +				focus $old_focus +			} +		} +		return +	} + +	if {![winfo exists $sb]} { +		if {$orient eq {h}} { +			ttk::scrollbar $sb -orient h -command [list $w_list xview] +			pack $sb -fill x -side bottom -before $w_list +		} else { +			ttk::scrollbar $sb -orient v -command [list $w_list yview] +			pack $sb -fill y -side right -before $w_list +		} +		if {$old_focus ne {}} { +			update +			focus $old_focus +		} +	} + +	catch {$sb set $first $last} +} + +method _show_tooltip {pos} { +	if {$tooltip_wm ne {}} { +		_open_tooltip $this +	} elseif {$tooltip_timer eq {}} { +		set tooltip_timer [after 1000 [cb _open_tooltip]] +	} +} + +method _open_tooltip {} { +	global remote_url + +	set tooltip_timer {} +	set pos_x [winfo pointerx $w_list] +	set pos_y [winfo pointery $w_list] +	if {[winfo containing $pos_x $pos_y] ne $w_list} { +		_hide_tooltip $this +		return +	} + +	set pos @[join [list \ +		[expr {$pos_x - [winfo rootx $w_list]}] \ +		[expr {$pos_y - [winfo rooty $w_list]}]] ,] +	set lno [$w_list index $pos] +	if {$lno eq {}} { +		_hide_tooltip $this +		return +	} + +	set spec [lindex $cur_specs $lno] +	set refn [lindex $spec 1] +	if {$refn eq {}} { +		_hide_tooltip $this +		return +	} + +	if {$tooltip_wm eq {}} { +		set tooltip_wm [toplevel $w_list.tooltip -borderwidth 1] +		catch {wm attributes $tooltip_wm -type tooltip} +		wm overrideredirect $tooltip_wm 1 +		wm transient $tooltip_wm [winfo toplevel $w_list] +		set tooltip_t $tooltip_wm.label +		text $tooltip_t \ +			-takefocus 0 \ +			-highlightthickness 0 \ +			-relief flat \ +			-borderwidth 0 \ +			-wrap none \ +			-background lightyellow \ +			-foreground black +		$tooltip_t tag conf section_header -font font_uibold +		bind $tooltip_wm <Escape> [cb _hide_tooltip] +		pack $tooltip_t +	} else { +		$tooltip_t conf -state normal +		$tooltip_t delete 0.0 end +	} + +	set data $tip_data($refn) +	if {[lindex $data 0 0] eq {tag}} { +		set tag  [lindex $data 0] +		if {[lindex $data 1 0] eq {commit}} { +			set cmit [lindex $data 1] +		} else { +			set cmit {} +		} +	} elseif {[lindex $data 0 0] eq {commit}} { +		set tag  {} +		set cmit [lindex $data 0] +	} + +	$tooltip_t insert end [lindex $spec 0] +	set last [_reflog_last $this [lindex $spec 1]] +	if {$last ne {}} { +		$tooltip_t insert end "\n" +		$tooltip_t insert end [mc "Updated"] +		$tooltip_t insert end " $last" +	} +	$tooltip_t insert end "\n" + +	if {$tag ne {}} { +		$tooltip_t insert end "\n" +		$tooltip_t insert end [mc "Tag"] section_header +		$tooltip_t insert end "  [lindex $tag 1]\n" +		$tooltip_t insert end [lindex $tag 2] +		$tooltip_t insert end " ([lindex $tag 3])\n" +		$tooltip_t insert end [lindex $tag 4] +		$tooltip_t insert end "\n" +	} + +	if {$cmit ne {}} { +		$tooltip_t insert end "\n" +		$tooltip_t insert end [mc "Commit@@noun"] section_header +		$tooltip_t insert end "  [lindex $cmit 1]\n" +		$tooltip_t insert end [lindex $cmit 2] +		$tooltip_t insert end " ([lindex $cmit 3])\n" +		$tooltip_t insert end [lindex $cmit 4] +	} + +	if {[llength $spec] > 2} { +		$tooltip_t insert end "\n" +		$tooltip_t insert end [mc "Remote"] section_header +		$tooltip_t insert end "  [lindex $spec 2]\n" +		$tooltip_t insert end [mc "URL"] +		$tooltip_t insert end " $remote_url([lindex $spec 2])\n" +		$tooltip_t insert end [mc "Branch"] +		$tooltip_t insert end " [lindex $spec 3]" +	} + +	$tooltip_t conf -state disabled +	_position_tooltip $this +} + +method _reflog_last {name} { +	if {[info exists reflog_last($name)]} { +		return reflog_last($name) +	} + +	set last {} +	if {[catch {set last [file mtime [gitdir $name]]}] +	&& ![catch {set g [safe_open_file [gitdir logs $name] r]}]} { +		fconfigure $g -encoding iso8859-1 +		while {[gets $g line] >= 0} { +			if {[regexp {> ([1-9][0-9]*) } $line line when]} { +				set last $when +			} +		} +		close $g +	} + +	if {$last ne {}} { +		set last [format_date $last] +	} +	set reflog_last($name) $last +	return $last +} + +method _position_tooltip {} { +	set max_h [lindex [split [$tooltip_t index end] .] 0] +	set max_w 0 +	for {set i 1} {$i <= $max_h} {incr i} { +		set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] +		if {$c > $max_w} {set max_w $c} +	} +	$tooltip_t conf -width $max_w -height $max_h + +	set req_w [winfo reqwidth  $tooltip_t] +	set req_h [winfo reqheight $tooltip_t] +	set pos_x [expr {[winfo pointerx .] +  5}] +	set pos_y [expr {[winfo pointery .] + 10}] + +	set g "${req_w}x${req_h}" +	if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +} +	append g $pos_x +	if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +} +	append g $pos_y + +	wm geometry $tooltip_wm $g +	raise $tooltip_wm +} + +method _hide_tooltip {} { +	if {$tooltip_wm ne {}} { +		destroy $tooltip_wm +		set tooltip_wm {} +	} +	if {$tooltip_timer ne {}} { +		after cancel $tooltip_timer +		set tooltip_timer {} +	} +} + +} diff --git a/git-gui/lib/chord.tcl b/git-gui/lib/chord.tcl new file mode 100644 index 0000000000..e21e7d3d0b --- /dev/null +++ b/git-gui/lib/chord.tcl @@ -0,0 +1,158 @@ +# Simple Chord for Tcl +# +# A "chord" is a method with more than one entrypoint and only one body, such +# that the body runs only once all the entrypoints have been called by +# different asynchronous tasks. In this implementation, the chord is defined +# dynamically for each invocation. A SimpleChord object is created, supplying +# body script to be run when the chord is completed, and then one or more notes +# are added to the chord. Each note can be called like a proc, and returns +# immediately if the chord isn't yet complete. When the last remaining note is +# called, the body runs before the note returns. +# +# The SimpleChord class has a constructor that takes the body script, and a +# method add_note that returns a note object. Since the body script does not +# run in the context of the procedure that defined it, a mechanism is provided +# for injecting variables into the chord for use by the body script. The +# activation of a note is idempotent; multiple calls have the same effect as +# a simple call. +# +# If you are invoking asynchronous operations with chord notes as completion +# callbacks, and there is a possibility that earlier operations could complete +# before later ones are started, it is a good practice to create a "common" +# note on the chord that prevents it from being complete until you're certain +# you've added all the notes you need. +# +# Example: +# +#   # Turn off the UI while running a couple of async operations. +#   lock_ui +# +#   set chord [SimpleChord::new { +#     unlock_ui +#     # Note: $notice here is not referenced in the calling scope +#     if {$notice} { info_popup $notice } +#   } +# +#   # Configure a note to keep the chord from completing until +#   # all operations have been initiated. +#   set common_note [$chord add_note] +# +#   # Activate notes in 'after' callbacks to other operations +#   set newnote [$chord add_note] +#   async_operation $args [list $newnote activate] +# +#   # Communicate with the chord body +#   if {$condition} { +#     # This sets $notice in the same context that the chord body runs in. +#     $chord eval { set notice "Something interesting" } +#   } +# +#   # Activate the common note, making the chord eligible to complete +#   $common_note activate +# +# At this point, the chord will complete at some unknown point in the future. +# The common note might have been the first note activated, or the async +# operations might have completed synchronously and the common note is the +# last one, completing the chord before this code finishes, or anything in +# between. The purpose of the chord is to not have to worry about the order. + +# SimpleChord class: +#   Represents a procedure that conceptually has multiple entrypoints that must +#   all be called before the procedure executes. Each entrypoint is called a +#   "note". The chord is only "completed" when all the notes are "activated". +class SimpleChord { +	field notes +	field body +	field is_completed +	field eval_ns + +	# Constructor: +	#   set chord [SimpleChord::new {body}] +	#     Creates a new chord object with the specified body script. The +	#     body script is evaluated at most once, when a note is activated +	#     and the chord has no other non-activated notes. +	constructor new {i_body} { +		set notes [list] +		set body $i_body +		set is_completed 0 +		set eval_ns "[namespace qualifiers $this]::eval" +		return $this +	} + +	# Method: +	#   $chord eval {script} +	#     Runs the specified script in the same context (namespace) in which +	#     the chord body will be evaluated. This can be used to set variable +	#     values for the chord body to use. +	method eval {script} { +		namespace eval $eval_ns $script +	} + +	# Method: +	#   set note [$chord add_note] +	#     Adds a new note to the chord, an instance of ChordNote. Raises an +	#     error if the chord is already completed, otherwise the chord is +	#     updated so that the new note must also be activated before the +	#     body is evaluated. +	method add_note {} { +		if {$is_completed} { error "Cannot add a note to a completed chord" } + +		set note [ChordNote::new $this] + +		lappend notes $note + +		return $note +	} + +	# This method is for internal use only and is intentionally undocumented. +	method notify_note_activation {} { +		if {!$is_completed} { +			foreach note $notes { +				if {![$note is_activated]} { return } +			} + +			set is_completed 1 + +			namespace eval $eval_ns $body +			delete_this +		} +	} +} + +# ChordNote class: +#   Represents a note within a chord, providing a way to activate it. When the +#   final note of the chord is activated (this can be any note in the chord, +#   with all other notes already previously activated in any order), the chord's +#   body is evaluated. +class ChordNote { +	field chord +	field is_activated + +	# Constructor: +	#   Instances of ChordNote are created internally by calling add_note on +	#   SimpleChord objects. +	constructor new {c} { +		set chord $c +		set is_activated 0 +		return $this +	} + +	# Method: +	#   [$note is_activated] +	#     Returns true if this note has already been activated. +	method is_activated {} { +		return $is_activated +	} + +	# Method: +	#   $note activate +	#     Activates the note, if it has not already been activated, and +	#     completes the chord if there are no other notes awaiting +	#     activation. Subsequent calls will have no further effect. +	method activate {} { +		if {!$is_activated} { +			set is_activated 1 +			$chord notify_note_activation +		} +	} +} diff --git a/git-gui/lib/class.tcl b/git-gui/lib/class.tcl new file mode 100644 index 0000000000..0b1e67103f --- /dev/null +++ b/git-gui/lib/class.tcl @@ -0,0 +1,193 @@ +# git-gui simple class/object fake-alike +# Copyright (C) 2007 Shawn Pearce + +proc class {class body} { +	if {[namespace exists $class]} { +		error "class $class already declared" +	} +	namespace eval $class " +		variable __nextid     0 +		variable __sealed     0 +		variable __field_list {} +		variable __field_array + +		proc cb {name args} { +			upvar this this +			concat \[list ${class}::\$name \$this\] \$args +		} +	" +	namespace eval $class $body +} + +proc field {name args} { +	set class [uplevel {namespace current}] +	variable ${class}::__sealed +	variable ${class}::__field_array + +	switch [llength $args] { +	0 { set new [list $name] } +	1 { set new [list $name [lindex $args 0]] } +	default { error "wrong # args: field name value?" } +	} + +	if {$__sealed} { +		error "class $class is sealed (cannot add new fields)" +	} + +	if {[catch {set old $__field_array($name)}]} { +		variable ${class}::__field_list +		lappend __field_list $new +		set __field_array($name) 1 +	} else { +		error "field $name already declared" +	} +} + +proc constructor {name params body} { +	set class [uplevel {namespace current}] +	set ${class}::__sealed 1 +	variable ${class}::__field_list +	set mbodyc {} + +	append mbodyc {set this } $class +	append mbodyc {::__o[incr } $class {::__nextid]::__d} \; +	append mbodyc {create_this } $class \; +	append mbodyc {set __this [namespace qualifiers $this]} \; + +	if {$__field_list ne {}} { +		append mbodyc {upvar #0} +		foreach n $__field_list { +			set n [lindex $n 0] +			append mbodyc { ${__this}::} $n { } $n +			regsub -all @$n\\M $body "\${__this}::$n" body +		} +		append mbodyc \; +		foreach n $__field_list { +			if {[llength $n] == 2} { +				append mbodyc \ +				{set } [lindex $n 0] { } [list [lindex $n 1]] \; +			} +		} +	} +	append mbodyc $body +	namespace eval $class [list proc $name $params $mbodyc] +} + +proc method {name params body {deleted {}} {del_body {}}} { +	set class [uplevel {namespace current}] +	set ${class}::__sealed 1 +	variable ${class}::__field_list +	set params [linsert $params 0 this] +	set mbodyc {} + +	append mbodyc {set __this [namespace qualifiers $this]} \; + +	switch $deleted { +	{} {} +	ifdeleted { +		append mbodyc {if {![namespace exists $__this]} } +		append mbodyc \{ $del_body \; return \} \; +	} +	default { +		error "wrong # args: method name args body (ifdeleted body)?" +	} +	} + +	set decl {} +	foreach n $__field_list { +		set n [lindex $n 0] +		if {[regexp -- $n\\M $body]} { +			if {   [regexp -all -- $n\\M $body] == 1 +				&& [regexp -all -- \\\$$n\\M $body] == 1 +				&& [regexp -all -- \\\$$n\\( $body] == 0} { +				regsub -all \ +					\\\$$n\\M $body \ +					"\[set \${__this}::$n\]" body +			} else { +				append decl { ${__this}::} $n { } $n +				regsub -all @$n\\M $body "\${__this}::$n" body +			} +		} +	} +	if {$decl ne {}} { +		append mbodyc {upvar #0} $decl \; +	} +	append mbodyc $body +	namespace eval $class [list proc $name $params $mbodyc] +} + +proc create_this {class} { +	upvar this this +	namespace eval [namespace qualifiers $this] [list proc \ +		[namespace tail $this] \ +		[list name args] \ +		"eval \[list ${class}::\$name $this\] \$args" \ +	] +} + +proc delete_this {{t {}}} { +	if {$t eq {}} { +		upvar this this +		set t $this +	} +	set t [namespace qualifiers $t] +	if {[namespace exists $t]} {namespace delete $t} +} + +proc make_dialog {t w args} { +	upvar $t top $w pfx this this +	uplevel [linsert $args 0 make_toplevel $t $w] +	catch {wm attributes $top -type dialog} +	pave_toplevel $pfx +} + +proc make_toplevel {t w args} { +	upvar $t top $w pfx this this + +	if {[llength $args] % 2} { +		error "make_toplevel topvar winvar {options}" +	} +	set autodelete 1 +	foreach {name value} $args { +		switch -exact -- $name { +		-autodelete {set autodelete $value} +		default     {error "unsupported option $name"} +		} +	} + +	if {$::root_exists || [winfo ismapped .]} { +		regsub -all {::} $this {__} w +		set top .$w +		set pfx $top +		toplevel $top +		set ::root_exists 1 +	} else { +		set top . +		set pfx {} +	} + +	if {$autodelete} { +		wm protocol $top WM_DELETE_WINDOW " +			[list delete_this $this] +			[list destroy $top] +		" +	} +} + + +## auto_mkindex support for class/constructor/method +## +auto_mkindex_parser::command class {name body} { +	variable parser +	variable contextStack +	set contextStack [linsert $contextStack 0 $name] +	$parser eval [list _%@namespace eval $name] $body +	set contextStack [lrange $contextStack 1 end] +} +auto_mkindex_parser::command constructor {name args} { +	variable index +	variable scriptFile +	append index [list set auto_index([fullname $name])] \ +		[format { [list source [file join $dir %s]]} \ +		[file split $scriptFile]] "\n" +} diff --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl new file mode 100644 index 0000000000..89eb8c7b73 --- /dev/null +++ b/git-gui/lib/commit.tcl @@ -0,0 +1,581 @@ +# git-gui misc. commit reading/writing support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc load_last_commit {} { +	global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author +	global repo_config + +	if {[llength $PARENT] == 0} { +		error_popup [mc "There is nothing to amend. + +You are about to create the initial commit.  There is no commit before this to amend. +"] +		return +	} + +	repository_state curType curHEAD curMERGE_HEAD +	if {$curType eq {merge}} { +		error_popup [mc "Cannot amend while merging. + +You are currently in the middle of a merge that has not been fully completed.  You cannot amend the prior commit unless you first abort the current merge activity. +"] +		return +	} + +	set msg {} +	set parents [list] +	if {[catch { +			set name "" +			set email "" +			set fd [git_read [list cat-file commit $curHEAD]] +			fconfigure $fd -encoding iso8859-1 +			# By default commits are assumed to be in utf-8 +			set enc utf-8 +			while {[gets $fd line] > 0} { +				if {[string match {parent *} $line]} { +					lappend parents [string range $line 7 end] +				} elseif {[string match {encoding *} $line]} { +					set enc [string tolower [string range $line 9 end]] +				} elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} { } +			} +			set msg [read $fd] +			close $fd + +			set enc [tcl_encoding $enc] +			if {$enc ne {}} { +				set msg [convertfrom $enc $msg] +				set name [convertfrom $enc $name] +				set email [convertfrom $enc $email] +			} +			if {$name ne {} && $email ne {}} { +				set commit_author [list name $name email $email date $time] +			} + +			set msg [string trim $msg] +		} err]} { +		error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"] +		return +	} + +	set HEAD $curHEAD +	set PARENT $parents +	set MERGE_HEAD [list] +	switch -- [llength $parents] { +	0       {set commit_type amend-initial} +	1       {set commit_type amend} +	default {set commit_type amend-merge} +	} + +	$ui_comm delete 0.0 end +	$ui_comm insert end $msg +	$ui_comm edit reset +	$ui_comm edit modified false +	rescan ui_ready +} + +set GIT_COMMITTER_IDENT {} + +proc committer_ident {} { +	global GIT_COMMITTER_IDENT + +	if {$GIT_COMMITTER_IDENT eq {}} { +		if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} { +			error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"] +			return {} +		} +		if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \ +			$me me GIT_COMMITTER_IDENT]} { +			error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"] +			return {} +		} +	} + +	return $GIT_COMMITTER_IDENT +} + +proc do_signoff {} { +	global ui_comm + +	set me [committer_ident] +	if {$me eq {}} return + +	set sob "Signed-off-by: $me" +	set last [$ui_comm get {end -1c linestart} {end -1c}] +	if {$last ne $sob} { +		$ui_comm edit separator +		if {$last ne {} +			&& ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} { +			$ui_comm insert end "\n" +		} +		$ui_comm insert end "\n$sob" +		$ui_comm edit separator +		$ui_comm see end +	} +} + +proc create_new_commit {} { +	global commit_type ui_comm commit_author + +	set commit_type normal +	unset -nocomplain commit_author +	$ui_comm delete 0.0 end +	$ui_comm edit reset +	$ui_comm edit modified false +	rescan ui_ready +} + +proc setup_commit_encoding {msg_wt {quiet 0}} { +	global repo_config + +	if {[catch {set enc $repo_config(i18n.commitencoding)}]} { +		set enc utf-8 +	} +	set use_enc [tcl_encoding $enc] +	if {$use_enc ne {}} { +		fconfigure $msg_wt -encoding $use_enc +	} else { +		if {!$quiet} { +			error_popup [mc "warning: Tcl does not support encoding '%s'." $enc] +		} +		fconfigure $msg_wt -encoding utf-8 +	} +} + +proc commit_tree {} { +	global HEAD commit_type file_states ui_comm repo_config +	global pch_error + +	if {[committer_ident] eq {}} return +	if {![lock_index update]} return + +	# -- Our in memory state should match the repository. +	# +	repository_state curType curHEAD curMERGE_HEAD +	if {[string match amend* $commit_type] +		&& $curType eq {normal} +		&& $curHEAD eq $HEAD} { +	} elseif {$commit_type ne $curType || $HEAD ne $curHEAD} { +		info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created. + +The rescan will be automatically started now. +"] +		unlock_index +		rescan ui_ready +		return +	} + +	# -- At least one file should differ in the index. +	# +	set files_ready 0 +	foreach path [array names file_states] { +		set s $file_states($path) +		switch -glob -- [lindex $s 0] { +		_? {continue} +		A? - +		D? - +		T? - +		M? {set files_ready 1} +		_U - +		U? { +			error_popup [mc "Unmerged files cannot be committed. + +File %s has merge conflicts.  You must resolve them and stage the file before committing. +" [short_path $path]] +			unlock_index +			return +		} +		default { +			error_popup [mc "Unknown file state %s detected. + +File %s cannot be committed by this program. +" [lindex $s 0] [short_path $path]] +		} +		} +	} +	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. +"] +		unlock_index +		return +	} + +	if {[is_enabled nocommitmsg]} { do_quit 0 } + +	# -- A message is required. +	# +	set msg [$ui_comm get 1.0 end] + +	# -- Build the message file. +	# +	set msg_p [gitdir GITGUI_EDITMSG] +	set msg_wt [safe_open_file $msg_p w] +	fconfigure $msg_wt -translation lf +	setup_commit_encoding $msg_wt +	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] +	if {$fd_ph eq {}} { +		commit_commitmsg $curHEAD $msg_p +		return +	} + +	ui_status [mc "Calling pre-commit hook..."] +	set pch_error {} +	fconfigure $fd_ph -blocking 0 -translation binary +	fileevent $fd_ph readable \ +		[list commit_prehook_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_prehook_wait {fd_ph curHEAD msg_p} { +	global pch_error + +	append pch_error [read $fd_ph] +	fconfigure $fd_ph -blocking 1 +	if {[eof $fd_ph]} { +		if {[catch {close $fd_ph}]} { +			catch {file delete $msg_p} +			ui_status [mc "Commit declined by pre-commit hook."] +			hook_failed_popup pre-commit $pch_error +			unlock_index +		} else { +			commit_commitmsg $curHEAD $msg_p +		} +		set pch_error {} +		return +	} +	fconfigure $fd_ph -blocking 0 +} + +proc commit_commitmsg {curHEAD msg_p} { +	global is_detached repo_config +	global pch_error + +	if {$is_detached +	    && ![file exists [gitdir rebase-merge head-name]] +	    && 	[is_config_true gui.warndetachedcommit]} { +		set msg [mc "You are about to commit on a detached head.\ +This is a potentially dangerous thing to do because if you switch\ +to another branch you will lose your changes and it can be difficult\ +to retrieve them later from the reflog. You should probably cancel this\ +commit and create a new branch to continue.\n\ +\n\ +Do you really want to proceed with your Commit?"] +		if {[ask_popup $msg] ne yes} { +			unlock_index +			return +		} +	} + +	# -- Run the commit-msg hook. +	# +	set fd_ph [githook_read commit-msg $msg_p] +	if {$fd_ph eq {}} { +		commit_writetree $curHEAD $msg_p +		return +	} + +	ui_status [mc "Calling commit-msg hook..."] +	set pch_error {} +	fconfigure $fd_ph -blocking 0 -translation binary +	fileevent $fd_ph readable \ +		[list commit_commitmsg_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_commitmsg_wait {fd_ph curHEAD msg_p} { +	global pch_error + +	append pch_error [read $fd_ph] +	fconfigure $fd_ph -blocking 1 +	if {[eof $fd_ph]} { +		if {[catch {close $fd_ph}]} { +			catch {file delete $msg_p} +			ui_status [mc "Commit declined by commit-msg hook."] +			hook_failed_popup commit-msg $pch_error +			unlock_index +		} else { +			commit_writetree $curHEAD $msg_p +		} +		set pch_error {} +		return +	} +	fconfigure $fd_ph -blocking 0 +} + +proc wash_commit_message {msg} { +	# Strip trailing whitespace +	regsub -all -line {[ \t\r]+$} $msg {} msg +	# Strip comment lines +	global comment_string +	set cmt_rx [strcat {(^|\n)} [regsub -all {\W} $comment_string {\\&}] {[^\n]*}] +	regsub -all $cmt_rx $msg {\1} msg +	# Strip leading and trailing empty lines (puts adds one \n) +	set msg [string trim $msg \n] +	# Compress consecutive empty lines +	regsub -all {\n{3,}} $msg \n\n msg + +	return $msg +} + +proc commit_writetree {curHEAD msg_p} { +	# -- Process the commit message after hooks have run. +	# +	set msg_fd [safe_open_file $msg_p r] +	setup_commit_encoding $msg_fd 1 +	set msg [read $msg_fd] +	close $msg_fd + +	# Process the message (strip whitespace, comments, etc.) +	set msg [wash_commit_message $msg] + +	if {$msg eq {}} { +		error_popup [mc "Please supply a commit message. + +A good commit message has the following format: + +- First line: Describe in one sentence what you did. +- Second line: Blank +- Remaining lines: Describe why this change is good. +"] +		unlock_index +		return +	} + +	# Write the processed message back to the file +	set msg_wt [safe_open_file $msg_p w] +	fconfigure $msg_wt -translation lf +	setup_commit_encoding $msg_wt +	puts $msg_wt $msg +	close $msg_wt + +	ui_status [mc "Committing changes..."] +	set fd_wt [git_read [list write-tree]] +	fileevent $fd_wt readable \ +		[list commit_committree $fd_wt $curHEAD $msg_p] +} + +proc commit_committree {fd_wt curHEAD msg_p} { +	global HEAD PARENT MERGE_HEAD commit_type commit_author +	global current_branch +	global ui_comm commit_type_is_amend +	global file_states selected_paths rescan_active +	global repo_config +	global env +	global hashlength + +	gets $fd_wt tree_id +	if {[catch {close $fd_wt} err]} { +		catch {file delete $msg_p} +		error_popup [strcat [mc "write-tree failed:"] "\n\n$err"] +		ui_status [mc "Commit failed."] +		unlock_index +		return +	} + +	# -- Verify this wasn't an empty change. +	# +	if {$commit_type eq {normal}} { +		set fd_ot [git_read [list cat-file commit $PARENT]] +		fconfigure $fd_ot -encoding iso8859-1 +		set old_tree [gets $fd_ot] +		close $fd_ot + +		if {[string equal -length 5 {tree } $old_tree] +			&& [string length $old_tree] == [expr {$hashlength + 5}]} { +			set old_tree [string range $old_tree 5 end] +		} else { +			error [mc "Commit %s appears to be corrupt" $PARENT] +		} + +		if {$tree_id eq $old_tree} { +			catch {file delete $msg_p} +			info_popup [mc "No changes to commit. + +No files were modified by this commit and it was not a merge commit. + +A rescan will be automatically started now. +"] +			unlock_index +			rescan {ui_status [mc "No changes to commit."]} +			return +		} +	} + +	if {[info exists commit_author]} { +		set old_author [commit_author_ident $commit_author] +	} +	# -- Create the commit. +	# +	set cmd [list commit-tree $tree_id] +	if {[is_config_true commit.gpgsign]} { +		lappend cmd -S +	} +	foreach p [concat $PARENT $MERGE_HEAD] { +		lappend cmd -p $p +	} +	set msgtxt [list <$msg_p] +	if {[catch {set cmt_id [git_redir $cmd $msgtxt]} err]} { +		catch {file delete $msg_p} +		error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"] +		ui_status [mc "Commit failed."] +		unlock_index +		unset -nocomplain commit_author +		commit_author_reset $old_author +		return +	} +	if {[info exists commit_author]} { +		unset -nocomplain commit_author +		commit_author_reset $old_author +	} + +	# -- Update the HEAD ref. +	# +	set reflogm commit +	if {$commit_type ne {normal}} { +		append reflogm " ($commit_type)" +	} +	set msg_fd [safe_open_file $msg_p r] +	setup_commit_encoding $msg_fd 1 +	gets $msg_fd subject +	close $msg_fd +	append reflogm {: } $subject +	if {[catch { +			git update-ref -m $reflogm HEAD $cmt_id $curHEAD +		} err]} { +		catch {file delete $msg_p} +		error_popup [strcat [mc "update-ref failed:"] "\n\n$err"] +		ui_status [mc "Commit failed."] +		unlock_index +		return +	} + +	# -- Cleanup after ourselves. +	# +	catch {file delete $msg_p} +	catch {file delete [gitdir MERGE_HEAD]} +	catch {file delete [gitdir MERGE_MSG]} +	catch {file delete [gitdir SQUASH_MSG]} +	catch {file delete [gitdir GITGUI_MSG]} +	catch {file delete [gitdir CHERRY_PICK_HEAD]} + +	# -- Let rerere do its thing. +	# +	if {[get_config rerere.enabled] eq {}} { +		set rerere [file isdirectory [gitdir rr-cache]] +	} else { +		set rerere [is_config_true rerere.enabled] +	} +	if {$rerere} { +		catch {git rerere} +	} + +	# -- Run the post-commit hook. +	# +	set fd_ph [githook_read post-commit] +	if {$fd_ph ne {}} { +		global pch_error +		set pch_error {} +		fconfigure $fd_ph -blocking 0 -translation binary +		fileevent $fd_ph readable \ +			[list commit_postcommit_wait $fd_ph $cmt_id] +	} + +	$ui_comm delete 0.0 end +	load_message [get_config commit.template] +	$ui_comm edit reset +	$ui_comm edit modified false +	if {$::GITGUI_BCK_exists} { +		catch {file delete [gitdir GITGUI_BCK]} +		set ::GITGUI_BCK_exists 0 +	} + +	if {[is_enabled singlecommit]} { do_quit 0 } + +	# -- Update in memory status +	# +	set commit_type normal +	set commit_type_is_amend 0 +	set HEAD $cmt_id +	set PARENT $cmt_id +	set MERGE_HEAD [list] + +	foreach path [array names file_states] { +		set s $file_states($path) +		set m [lindex $s 0] +		switch -glob -- $m { +		_O - +		_M - +		_D {continue} +		__ - +		A_ - +		M_ - +		T_ - +		D_ { +			unset file_states($path) +			catch {unset selected_paths($path)} +		} +		DO { +			set file_states($path) [list _O [lindex $s 1] {} {}] +		} +		AM - +		AD - +		AT - +		TM - +		TD - +		MM - +		MT - +		MD { +			set file_states($path) [list \ +				_[string index $m 1] \ +				[lindex $s 1] \ +				[lindex $s 3] \ +				{}] +		} +		} +	} + +	display_all_files +	unlock_index +	reshow_diff +	ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject] +} + +proc commit_postcommit_wait {fd_ph cmt_id} { +	global pch_error + +	append pch_error [read $fd_ph] +	fconfigure $fd_ph -blocking 1 +	if {[eof $fd_ph]} { +		if {[catch {close $fd_ph}]} { +			hook_failed_popup post-commit $pch_error 0 +		} +		unset pch_error +		return +	} +	fconfigure $fd_ph -blocking 0 +} + +proc commit_author_ident {details} { +	global env +	array set author $details +	set old [array get env GIT_AUTHOR_*] +	set env(GIT_AUTHOR_NAME) $author(name) +	set env(GIT_AUTHOR_EMAIL) $author(email) +	set env(GIT_AUTHOR_DATE) $author(date) +	return $old +} +proc commit_author_reset {details} { +	global env +	unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE) +	if {$details ne {}} { +		array set env $details +	} +} diff --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl new file mode 100644 index 0000000000..267699408c --- /dev/null +++ b/git-gui/lib/console.tcl @@ -0,0 +1,223 @@ +# git-gui console support +# Copyright (C) 2006, 2007 Shawn Pearce + +class console { + +field t_short +field t_long +field w +field w_t +field console_cr +field is_toplevel    1; # are we our own window? + +constructor new {short_title long_title} { +	set t_short $short_title +	set t_long $long_title +	_init $this +	return $this +} + +constructor embed {path title} { +	set t_short {} +	set t_long $title +	set w $path +	set is_toplevel 0 +	_init $this +	return $this +} + +method _init {} { +	global M1B + +	if {$is_toplevel} { +		make_dialog top w -autodelete 0 +		wm title $top "[appname] ([reponame]): $t_short" +	} else { +		ttk::frame $w +	} + +	set console_cr 1.0 +	set w_t $w.m.t + +	ttk::frame $w.m +	ttk::label $w.m.l1 \ +		-textvariable @t_long  \ +		-anchor w \ +		-justify left \ +		-font font_uibold +	text $w_t \ +		-background white \ +		-foreground black \ +		-borderwidth 1 \ +		-relief sunken \ +		-width 80 -height 10 \ +		-wrap none \ +		-font font_diff \ +		-state disabled \ +		-xscrollcommand [cb _sb_set $w.m.sbx h] \ +		-yscrollcommand [cb _sb_set $w.m.sby v] +	label $w.m.s -text [mc "Working... please wait..."] \ +		-anchor w \ +		-justify left \ +		-font font_uibold +	pack $w.m.l1 -side top -fill x +	pack $w.m.s -side bottom -fill x +	pack $w_t -side left -fill both -expand 1 +	pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10 + +	menu $w.ctxm -tearoff 0 +	$w.ctxm add command -label [mc "Copy"] \ +		-command "tk_textCopy $w_t" +	$w.ctxm add command -label [mc "Select All"] \ +		-command "focus $w_t;$w_t tag add sel 0.0 end" +	$w.ctxm add command -label [mc "Copy All"] \ +		-command " +			$w_t tag add sel 0.0 end +			tk_textCopy $w_t +			$w_t tag remove sel 0.0 end +		" + +	if {$is_toplevel} { +		ttk::button $w.ok -text [mc "Close"] \ +			-state disabled \ +			-command [list destroy $w] +		pack $w.ok -side bottom -anchor e -pady 10 -padx 10 +		bind $w <Visibility> [list focus $w] +	} + +	bind_button3 $w_t "tk_popup $w.ctxm %X %Y" +	bind $w_t <$M1B-Key-a> "$w_t tag add sel 0.0 end;break" +	bind $w_t <$M1B-Key-A> "$w_t tag add sel 0.0 end;break" +} + +method exec {cmd {after {}}} { +	if {[lindex $cmd 0] eq {git}} { +		set fd_f [git_read [lrange $cmd 1 end] [list 2>@1]] +	} else { +		set fd_f [safe_open_command $cmd [list 2>@1]] +	} +	fconfigure $fd_f -blocking 0 -translation binary -encoding [encoding system] +	fileevent $fd_f readable [cb _read $fd_f $after] +} + +method _read {fd after} { +	set buf [read $fd] +	if {$buf ne {}} { +		if {![winfo exists $w_t]} {_init $this} +		$w_t conf -state normal +		set c 0 +		set n [string length $buf] +		while {$c < $n} { +			set cr [string first "\r" $buf $c] +			set lf [string first "\n" $buf $c] +			if {$cr < 0} {set cr [expr {$n + 1}]} +			if {$lf < 0} {set lf [expr {$n + 1}]} + +			if {$lf < $cr} { +				$w_t insert end [string range $buf $c $lf] +				set console_cr [$w_t index {end -1c}] +				set c $lf +				incr c +			} else { +				$w_t delete $console_cr end +				$w_t insert end "\n" +				$w_t insert end [string range $buf $c [expr {$cr - 1}]] +				set c $cr +				incr c +			} +		} +		$w_t conf -state disabled +		$w_t see end +	} + +	fconfigure $fd -blocking 1 +	if {[eof $fd]} { +		if {[catch {close $fd}]} { +			set ok 0 +		} else { +			set ok 1 +		} +		if {$after ne {}} { +			uplevel #0 $after $ok +		} else { +			done $this $ok +		} +		return +	} +	fconfigure $fd -blocking 0 +} + +method chain {cmdlist {ok 1}} { +	if {$ok} { +		if {[llength $cmdlist] == 0} { +			done $this $ok +			return +		} + +		set cmd [lindex $cmdlist 0] +		set cmdlist [lrange $cmdlist 1 end] + +		if {[lindex $cmd 0] eq {exec}} { +			exec $this \ +				[lrange $cmd 1 end] \ +				[cb chain $cmdlist] +		} else { +			uplevel #0 $cmd [cb chain $cmdlist] +		} +	} else { +		done $this $ok +	} +} + +method insert {txt} { +	if {![winfo exists $w_t]} {_init $this} +	$w_t conf -state normal +	$w_t insert end "$txt\n" +	set console_cr [$w_t index {end -1c}] +	$w_t conf -state disabled +} + +method done {ok} { +	if {$ok} { +		if {[winfo exists $w.m.s]} { +			bind $w.m.s <Destroy> [list delete_this $this] +			$w.m.s conf -background green -foreground black \ +				-text [mc "Success"] +			if {$is_toplevel} { +				$w.ok conf -state normal +				focus $w.ok +			} +		} else { +			delete_this +		} +	} else { +		if {![winfo exists $w.m.s]} { +			_init $this +		} +		bind $w.m.s <Destroy> [list delete_this $this] +		$w.m.s conf -background red -foreground black \ +			-text [mc "Error: Command Failed"] +		if {$is_toplevel} { +			$w.ok conf -state normal +			focus $w.ok +		} +	} + +	bind $w <Key-Escape> "destroy $w;break" +} + +method _sb_set {sb orient first last} { +	if {![winfo exists $sb]} { +		if {$first == $last || ($first == 0 && $last == 1)} return +		if {$orient eq {h}} { +			ttk::scrollbar $sb -orient h -command [list $w_t xview] +			pack $sb -fill x -side bottom -before $w_t +		} else { +			ttk::scrollbar $sb -orient v -command [list $w_t yview] +			pack $sb -fill y -side right -before $w_t +		} +	} +	$sb set $first $last +} + +} diff --git a/git-gui/lib/database.tcl b/git-gui/lib/database.tcl new file mode 100644 index 0000000000..78732d8651 --- /dev/null +++ b/git-gui/lib/database.tcl @@ -0,0 +1,114 @@ +# git-gui object database management support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_stats {} { +	set fd [git_read [list count-objects -v]] +	while {[gets $fd line] > 0} { +		if {[regexp {^([^:]+): (\d+)$} $line _ name value]} { +			set stats($name) $value +		} +	} +	close $fd + +	set packed_sz 0 +	foreach p [glob -directory [gitdir objects pack] \ +		-type f \ +		-nocomplain -- *] { +		incr packed_sz [file size $p] +	} +	if {$packed_sz > 0} { +		set stats(size-pack) [expr {$packed_sz / 1024}] +	} + +	set w .stats_view +	Dialog $w +	wm withdraw $w +	wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + +	ttk::frame $w.buttons +	ttk::button $w.buttons.close -text [mc Close] \ +		-default active \ +		-command [list destroy $w] +	ttk::button $w.buttons.gc -text [mc "Compress Database"] \ +		-default normal \ +		-command "destroy $w;do_gc" +	pack $w.buttons.close -side right +	pack $w.buttons.gc -side left +	pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + +	ttk::labelframe $w.stat -text [mc "Database Statistics"] +	foreach s { +		{count           {mc "Number of loose objects"}} +		{size            {mc "Disk space used by loose objects"} { KiB}} +		{in-pack         {mc "Number of packed objects"}} +		{packs           {mc "Number of packs"}} +		{size-pack       {mc "Disk space used by packed objects"} { KiB}} +		{prune-packable  {mc "Packed objects waiting for pruning"}} +		{garbage         {mc "Garbage files"}} +		} { +		set name [lindex $s 0] +		set label [eval [lindex $s 1]] +		if {[catch {set value $stats($name)}]} continue +		if {[llength $s] > 2} { +			set value "$value[lindex $s 2]" +		} + +		ttk::label $w.stat.l_$name -text [mc "%s:" $label] -anchor w +		ttk::label $w.stat.v_$name -text $value -anchor w +		grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5} +	} +	pack $w.stat -pady 10 -padx 10 + +	bind $w <Visibility> "grab $w; focus $w.buttons.close" +	bind $w <Key-Escape> [list destroy $w] +	bind $w <Key-Return> [list destroy $w] +	wm title $w [mc "%s (%s): Database Statistics" [appname] [reponame]] +	wm deiconify $w +	tkwait window $w +} + +proc do_gc {} { +	set w [console::new {gc} [mc "Compressing the object database"]] +	console::chain $w { +		{exec git pack-refs --prune} +		{exec git reflog expire --all} +		{exec git repack -a -d -l} +		{exec git rerere gc} +	} +} + +proc do_fsck_objects {} { +	set w [console::new {fsck-objects} \ +		[mc "Verifying the object database with fsck-objects"]] +	set cmd [list git fsck-objects] +	lappend cmd --full +	lappend cmd --cache +	lappend cmd --strict +	console::exec $w $cmd +} + +proc hint_gc {} { +	set ndirs 1 +	set limit 8 +	if {[is_Windows]} { +		set ndirs 4 +		set limit 1 +	} + +	set count [llength [glob \ +		-nocomplain \ +		-- \ +		[gitdir objects 4\[0-[expr {$ndirs-1}]\]/*]]] + +	if {$count >= $limit * $ndirs} { +		set objects_current [expr {$count * 256/$ndirs}] +		if {[ask_popup \ +			[mc "This repository currently has approximately %i loose objects. + +To maintain optimal performance it is strongly recommended that you compress the database. + +Compress the database now?" $objects_current]] eq yes} { +			do_gc +		} +	} +} diff --git a/git-gui/lib/date.tcl b/git-gui/lib/date.tcl new file mode 100644 index 0000000000..abe82992b6 --- /dev/null +++ b/git-gui/lib/date.tcl @@ -0,0 +1,53 @@ +# git-gui date processing support +# Copyright (C) 2007 Shawn Pearce + +set git_month(Jan)  1 +set git_month(Feb)  2 +set git_month(Mar)  3 +set git_month(Apr)  4 +set git_month(May)  5 +set git_month(Jun)  6 +set git_month(Jul)  7 +set git_month(Aug)  8 +set git_month(Sep)  9 +set git_month(Oct) 10 +set git_month(Nov) 11 +set git_month(Dec) 12 + +proc parse_git_date {s} { +	if {$s eq {}} { +		return {} +	} + +	if {![regexp \ +		{^... (...) (\d{1,2}) (\d\d):(\d\d):(\d\d) (\d{4}) ([+-]?)(\d\d)(\d\d)$} $s s \ +		month day hr mm ss yr ew tz_h tz_m]} { +		error [mc "Invalid date from Git: %s" $s] +	} + +	set s [clock scan [format {%4.4i%2.2i%2.2iT%2s%2s%2s} \ +			$yr $::git_month($month) $day \ +			$hr $mm $ss] \ +			-gmt 1] + +	regsub ^0 $tz_h {} tz_h +	regsub ^0 $tz_m {} tz_m +	switch -- $ew { +	-  {set ew +} +	+  {set ew -} +	{} {set ew -} +	} + +	return [expr "$s $ew ($tz_h * 3600 + $tz_m * 60)"] +} + +proc format_date {s} { +	if {$s eq {}} { +		return {} +	} +	return [clock format $s -format {%a %b %e %H:%M:%S %Y}] +} + +proc reformat_date {s} { +	return [format_date [parse_git_date $s]] +} diff --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl new file mode 100644 index 0000000000..442737ba4f --- /dev/null +++ b/git-gui/lib/diff.tcl @@ -0,0 +1,877 @@ +# git-gui diff viewer +# Copyright (C) 2006, 2007 Shawn Pearce + +proc apply_tab_size {{firsttab {}}} { +	global repo_config ui_diff + +	set w [font measure font_diff "0"] +	if {$firsttab != 0} { +		$ui_diff configure -tabs [list [expr {$firsttab * $w}] [expr {($firsttab + $repo_config(gui.tabsize)) * $w}]] +	} else { +		$ui_diff configure -tabs [expr {$repo_config(gui.tabsize) * $w}] +	} +} + +proc clear_diff {} { +	global ui_diff current_diff_path current_diff_header +	global ui_index ui_workdir + +	$ui_diff conf -state normal +	$ui_diff delete 0.0 end +	$ui_diff conf -state disabled + +	set current_diff_path {} +	set current_diff_header {} + +	$ui_index tag remove in_diff 0.0 end +	$ui_workdir tag remove in_diff 0.0 end +} + +proc reshow_diff {{after {}}} { +	global file_states file_lists +	global current_diff_path current_diff_side +	global ui_diff + +	set p $current_diff_path +	if {$p eq {}} { +		# No diff is being shown. +	} 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 $after +	} +} + +proc force_diff_encoding {enc} { +	global current_diff_path + +	if {$current_diff_path ne {}} { +		force_path_encoding $current_diff_path $enc +		reshow_diff +	} +} + +proc handle_empty_diff {} { +	global current_diff_path file_states +	global ui_diff + +	set path $current_diff_path +	set s $file_states($path) +	if {[lindex $s 0] ne {_M} || [has_textconv $path]} return + +	$ui_diff conf -state normal +	$ui_diff insert end [mc "* No differences detected; stage the file to de-list it from Unstaged Changes.\n"] d_info +	$ui_diff insert end [mc "* Click to find other files that may have the same state.\n"] d_rescan +	$ui_diff conf -state disabled +} + +proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} { +	global file_states file_lists +	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 + +	clear_diff +	if {$lno == {}} { +		set lno [lsearch -sorted -exact $file_lists($w) $path] +		if {$lno >= 0} { +			incr lno +		} +	} +	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_conflict_diff 0 +	set current_diff_path $path +	set current_diff_side $w +	set current_diff_queue {} +	ui_status [mc "Loading diff of %s..." [escape_path $path]] + +	set cont_info [list $scroll_pos $callback] + +	apply_tab_size 0 + +	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 +	} + +	global current_diff_path selected_paths +	set selected_paths($current_diff_path) 1 +} + +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 100000 +		set type unknown +		if {[catch { +				set type [file type $path] +				switch -- $type { +				directory { +					set type submodule +					set content {} +					set sz 0 +				} +				link { +					set content [file readlink $path] +					set sz [string length $content] +				} +				file { +					set fd [safe_open_file $path r] +					fconfigure $fd \ +						-encoding [get_path_encoding $path] +					set content [read $fd $max_sz] +					close $fd +					set sz [file size $path] +				} +				default { +					error "'$type' not supported" +				} +				} +			} err ]} { +			set diff_active 0 +			unlock_index +			ui_status [mc "Unable to display %s" [escape_path $path]] +			error_popup [strcat [mc "Error loading file:"] "\n\n$err"] +			return +		} +		$ui_diff conf -state normal +		if {$type eq {submodule}} { +			$ui_diff insert end \ +				"* [mc "Git Repository (subproject)"]\n" \ +				d_info +		} elseif {![catch {set type [safe_exec [list file $path]]}]} { +			set n [string length $path] +			if {[string equal -length $n $path $type]} { +				set type [string range $type $n end] +				regsub {^:?\s*} $type {} type +			} +			$ui_diff insert end "* $type\n" d_info +		} +		if {[string first "\0" $content] != -1} { +			$ui_diff insert end \ +				[mc "* Binary file (not showing content)."] \ +				d_info +		} else { +			if {$sz > $max_sz} { +				$ui_diff insert end [mc \ +"* Untracked file is %d bytes. +* Showing only first %d bytes. +" $sz $max_sz] d_info +			} +			$ui_diff insert end $content +			if {$sz > $max_sz} { +				$ui_diff insert end [mc " +* Untracked file clipped here by %s. +* To see the entire file, use an external editor. +" [appname]] d_info +			} +		} +		$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 is_submodule_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 is_submodule_diff 0 +	set diff_active 1 +	set current_diff_header {} +	set conflict_size [gitattr $path conflict-marker-size 7] + +	set cmd [list] +	if {$w eq $ui_index} { +		lappend cmd diff-index +		lappend cmd --cached +		lappend cmd --ignore-submodules=dirty +	} elseif {$w eq $ui_workdir} { +		if {[string first {U} $m] >= 0} { +			lappend cmd diff +		} else { +			lappend cmd diff-files +		} +	} +	if {![is_config_false gui.textconv]} { +		lappend cmd --textconv +	} + +	if {[string match {160000 *} [lindex $s 2]] +	 || [string match {160000 *} [lindex $s 3]]} { +		set is_submodule_diff 1 +		lappend cmd --submodule +	} + +	lappend cmd -p +	lappend cmd --color +	set cmd [concat $cmd $repo_config(gui.diffopts)] +	if {$repo_config(gui.diffcontext) >= 1} { +		lappend cmd "-U$repo_config(gui.diffcontext)" +	} +	if {$w eq $ui_index} { +		lappend cmd [PARENT] +	} +	if {$add_opts ne {}} { +		eval lappend cmd $add_opts +	} else { +		lappend cmd -- +		lappend cmd $path +	} + +	if {[catch {set fd [git_read_nice $cmd]} err]} { +		set diff_active 0 +		unlock_index +		ui_status [mc "Unable to display %s" [escape_path $path]] +		error_popup [strcat [mc "Error loading diff:"] "\n\n$err"] +		return +	} + +	set ::current_diff_inheader 1 +	# Detect pre-image lines of the diff3 conflict-style. They are just +	# '++' lines which is not bijective. Thus, we need to maintain a state +	# across lines. +	set ::conflict_in_pre_image 0 + +	# git-diff has eol==\n, \r if present is part of the text +	fconfigure $fd \ +		-blocking 0 \ +		-encoding [get_path_encoding $path] \ +		-translation lf +	fileevent $fd readable [list read_diff $fd $conflict_size $cont_info] +} + +proc parse_color_line {line} { +	set start 0 +	set result "" +	set markup [list] +	set regexp {\033\[((?:\d+;)*\d+)?m} +	set need_reset 0 +	while {[regexp -indices -start $start $regexp $line match code]} { +		foreach {begin end} $match break +		append result [string range $line $start [expr {$begin - 1}]] +		set pos [string length $result] +		set col [eval [linsert $code 0 string range $line]] +		set start [incr end] +		if {$col eq "0" || $col eq ""} { +			if {!$need_reset} continue +			set need_reset 0 +		} else { +			set need_reset 1 +		} +		lappend markup $pos $col +	} +	append result [string range $line $start end] +	if {[llength $markup] < 4} {set markup {}} +	return [list $result $markup] +} + +proc read_diff {fd conflict_size cont_info} { +	global ui_diff diff_active is_submodule_diff +	global is_3way_diff is_conflict_diff current_diff_header +	global current_diff_queue + +	$ui_diff conf -state normal +	while {[gets $fd line] >= 0} { +		foreach {line markup} [parse_color_line $line] break +		set line [string map {\033 ^} $line] + +		set tags {} + +		# -- Check for start of diff header. +		if {   [string match {diff --git *}      $line] +		    || [string match {diff --cc *}       $line] +		    || [string match {diff --combined *} $line]} { +			set ::current_diff_inheader 1 +		} + +		# -- Check for end of diff header (any hunk line will do this). +		# +		if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0} + +		# -- Automatically detect if this is a 3 way diff. +		# +		if {[string match {@@@ *} $line]} { +			set is_3way_diff 1 +			apply_tab_size 1 +		} + +		if {$::current_diff_inheader} { + +			# -- These two lines stop a diff header and shouldn't be in there +			if {   [string match {Binary files * and * differ} $line] +			    || [regexp {^\* Unmerged path }                $line]} { +				set ::current_diff_inheader 0 +			} else { +				append current_diff_header $line "\n" +			} + +			# -- Cleanup uninteresting diff header lines. +			# +			if {   [string match {diff --git *}      $line] +			    || [string match {diff --cc *}       $line] +			    || [string match {diff --combined *} $line] +			    || [string match {--- *}             $line] +			    || [string match {+++ *}             $line] +			    || [string match {index *}           $line]} { +				continue +			} + +			# -- Name it symlink, not 120000 +			#    Note, that the original line is in $current_diff_header +			regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line + +		} elseif {   $line eq {\ No newline at end of file}} { +			# -- Handle some special lines +		} elseif {$is_3way_diff} { +			set op [string range $line 0 1] +			switch -- $op { +			{  } {set tags {}} +			{@@} {set tags d_@} +			{ +} {set tags d_s+} +			{ -} {set tags d_s-} +			{+ } {set tags d_+s} +			{- } {set tags d_-s} +			{--} {set tags d_--} +			{++} { +				set regexp [string map [list %conflict_size $conflict_size]\ +								{^\+\+([<>=|]){%conflict_size}(?: |$)}] +				if {[regexp $regexp $line _g op]} { +					set is_conflict_diff 1 +					set line [string replace $line 0 1 {  }] +					set tags d$op + +					# The ||| conflict-marker marks the start of the pre-image. +					# All those lines are also prefixed with '++'. Thus we need +					# to maintain this state. +					set ::conflict_in_pre_image [expr {$op eq {|}}] +				} elseif {$::conflict_in_pre_image} { +					# This is a pre-image line. It is the one which both sides +					# are based on. As it has also the '++' line start, it is +					# normally shown as 'added'. Invert this to '--' to make +					# it a 'removed' line. +					set line [string replace $line 0 1 {--}] +					set tags d_-- +				} else { +					set tags d_++ +				} +			} +			default { +				puts "error: Unhandled 3 way diff marker: {$op}" +				set tags {} +			} +			} +		} elseif {$is_submodule_diff} { +			if {$line == ""} continue +			if {[regexp {^Submodule } $line]} { +				set tags d_info +			} elseif {[regexp {^\* } $line]} { +				set line [string replace $line 0 1 {Submodule }] +				set tags d_info +			} else { +				set op [string range $line 0 2] +				switch -- $op { +				{  <} {set tags d_-} +				{  >} {set tags d_+} +				{  W} {set tags {}} +				default { +					puts "error: Unhandled submodule diff marker: {$op}" +					set tags {} +				} +				} +			} +		} else { +			set op [string index $line 0] +			switch -- $op { +			{ } {set tags {}} +			{@} {set tags d_@} +			{-} {set tags d_-} +			{+} { +				set regexp [string map [list %conflict_size $conflict_size]\ +								{^\+([<>=]){%conflict_size}(?: |$)}] +				if {[regexp $regexp $line _g op]} { +					set is_conflict_diff 1 +					set tags d$op +				} else { +					set tags d_+ +				} +			} +			default { +				puts "error: Unhandled 2 way diff marker: {$op}" +				set tags {} +			} +			} +		} +		set mark [$ui_diff index "end - 1 line linestart"] +		$ui_diff insert end $line $tags +		if {[string index $line end] eq "\r"} { +			$ui_diff tag add d_cr {end - 2c} +		} +		$ui_diff insert end "\n" $tags + +		foreach {posbegin colbegin posend colend} $markup { +			set prefix clr +			foreach style [lsort -integer [split $colbegin ";"]] { +				if {$style eq "7"} {append prefix i; continue} +				if {$style != 4 && ($style < 30 || $style > 47)} {continue} +				set a "$mark linestart + $posbegin chars" +				set b "$mark linestart + $posend chars" +				catch {$ui_diff tag add $prefix$style $a $b} +			} +		} +	} +	$ui_diff conf -state disabled + +	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 +		} +		ui_ready + +		if {[$ui_diff index end] eq {2.0}} { +			handle_empty_diff +		} + +		set callback [lindex $cont_info 1] +		if {$callback ne {}} { +			eval $callback +		} +	} +} + +proc apply_or_revert_hunk {x y revert} { +	global current_diff_path current_diff_header current_diff_side +	global ui_diff ui_index file_states last_revert last_revert_enc + +	if {$current_diff_path eq {} || $current_diff_header eq {}} return +	if {![lock_index apply_hunk]} return + +	set apply_cmd {apply --whitespace=nowarn} +	set mi [lindex $file_states($current_diff_path) 0] +	if {$current_diff_side eq $ui_index} { +		set failed_msg [mc "Failed to unstage selected hunk."] +		lappend apply_cmd --reverse --cached +		if {[string index $mi 0] ne {M}} { +			unlock_index +			return +		} +	} else { +		if {$revert} { +			set failed_msg [mc "Failed to revert selected hunk."] +			lappend apply_cmd --reverse +		} else { +			set failed_msg [mc "Failed to stage selected hunk."] +			lappend apply_cmd --cached +		} + +		if {[string index $mi 1] ne {M}} { +			unlock_index +			return +		} +	} + +	set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0] +	set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0] +	if {$s_lno eq {}} { +		unlock_index +		return +	} + +	set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end] +	if {$e_lno eq {}} { +		set e_lno end +	} + +	set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]" + +	if {[catch { +		set enc [get_path_encoding $current_diff_path] +		set p [git_write $apply_cmd] +		fconfigure $p -translation binary -encoding $enc +		puts -nonewline $p $wholepatch +		close $p} err]} { +		error_popup "$failed_msg\n\n$err" +		unlock_index +		return +	} + +	if {$revert} { +		# Save a copy of this patch for undoing reverts. +		set last_revert $wholepatch +		set last_revert_enc $enc +	} + +	$ui_diff conf -state normal +	$ui_diff delete $s_lno $e_lno +	$ui_diff conf -state disabled + +	# Check if the hunk was the last one in the file. +	if {[$ui_diff get 1.0 end] eq "\n"} { +		set o _ +	} else { +		set o ? +	} + +	# Update the status flags. +	if {$revert} { +		set mi [string index $mi 0]$o +	} elseif {$current_diff_side eq $ui_index} { +		set mi ${o}M +	} elseif {[string index $mi 0] eq {_}} { +		set mi M$o +	} else { +		set mi ?$o +	} +	unlock_index +	display_file $current_diff_path $mi +	# This should trigger shift to the next changed file +	if {$o eq {_}} { +		reshow_diff +	} +} + +proc apply_or_revert_range_or_line {x y revert} { +	global current_diff_path current_diff_header current_diff_side +	global ui_diff ui_index file_states last_revert + +	set selected [$ui_diff tag nextrange sel 0.0] + +	if {$selected == {}} { +		set first [$ui_diff index "@$x,$y"] +		set last $first +	} else { +		set first [lindex $selected 0] +		set last [lindex $selected 1] +	} + +	set first_l [$ui_diff index "$first linestart"] +	set last_l [$ui_diff index "$last lineend"] + +	if {$current_diff_path eq {} || $current_diff_header eq {}} return +	if {![lock_index apply_hunk]} return + +	set apply_cmd {apply --whitespace=nowarn} +	set mi [lindex $file_states($current_diff_path) 0] +	if {$current_diff_side eq $ui_index} { +		set failed_msg [mc "Failed to unstage selected line."] +		set to_context {+} +		lappend apply_cmd --reverse --cached +		if {[string index $mi 0] ne {M}} { +			unlock_index +			return +		} +	} else { +		if {$revert} { +			set failed_msg [mc "Failed to revert selected line."] +			set to_context {+} +			lappend apply_cmd --reverse +		} else { +			set failed_msg [mc "Failed to stage selected line."] +			set to_context {-} +			lappend apply_cmd --cached +		} + +		if {[string index $mi 1] ne {M}} { +			unlock_index +			return +		} +	} + +	set wholepatch {} + +	while {$first_l < $last_l} { +		set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0] +		if {$i_l eq {}} { +			# If there's not a @@ above, then the selected range +			# must have come before the first_l @@ +			set i_l [$ui_diff search -regexp ^@@ $first_l $last_l] +		} +		if {$i_l eq {}} { +			unlock_index +			return +		} +		# $i_l is now at the beginning of a line + +		# pick start line number from hunk header +		set hh [$ui_diff get $i_l "$i_l + 1 lines"] +		set hh [lindex [split $hh ,] 0] +		set hln [lindex [split $hh -] 1] +		set hln [lindex [split $hln " "] 0] + +		# There is a special situation to take care of. Consider this +		# hunk: +		# +		#    @@ -10,4 +10,4 @@ +		#     context before +		#    -old 1 +		#    -old 2 +		#    +new 1 +		#    +new 2 +		#     context after +		# +		# We used to keep the context lines in the order they appear in +		# the hunk. But then it is not possible to correctly stage only +		# "-old 1" and "+new 1" - it would result in this staged text: +		# +		#    context before +		#    old 2 +		#    new 1 +		#    context after +		# +		# (By symmetry it is not possible to *un*stage "old 2" and "new +		# 2".) +		# +		# We resolve the problem by introducing an asymmetry, namely, +		# when a "+" line is *staged*, it is moved in front of the +		# context lines that are generated from the "-" lines that are +		# immediately before the "+" block. That is, we construct this +		# patch: +		# +		#    @@ -10,4 +10,5 @@ +		#     context before +		#    +new 1 +		#     old 1 +		#     old 2 +		#     context after +		# +		# But we do *not* treat "-" lines that are *un*staged in a +		# special way. +		# +		# With this asymmetry it is possible to stage the change "old +		# 1" -> "new 1" directly, and to stage the change "old 2" -> +		# "new 2" by first staging the entire hunk and then unstaging +		# the change "old 1" -> "new 1". +		# +		# Applying multiple lines adds complexity to the special +		# situation.  The pre_context must be moved after the entire +		# first block of consecutive staged "+" lines, so that +		# staging both additions gives the following patch: +		# +		#    @@ -10,4 +10,6 @@ +		#     context before +		#    +new 1 +		#    +new 2 +		#     old 1 +		#     old 2 +		#     context after + +		# This is non-empty if and only if we are _staging_ changes; +		# then it accumulates the consecutive "-" lines (after +		# converting them to context lines) in order to be moved after +		# "+" change lines. +		set pre_context {} + +		set n 0 +		set m 0 +		set i_l [$ui_diff index "$i_l + 1 lines"] +		set patch {} +		while {[$ui_diff compare $i_l < "end - 1 chars"] && +		       [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} { +			set next_l [$ui_diff index "$i_l + 1 lines"] +			set c1 [$ui_diff get $i_l] +			if {[$ui_diff compare $first_l <= $i_l] && +			    [$ui_diff compare $i_l < $last_l] && +			    ($c1 eq {-} || $c1 eq {+})} { +				# a line to stage/unstage +				set ln [$ui_diff get $i_l $next_l] +				if {$c1 eq {-}} { +					set n [expr $n+1] +					set patch "$patch$pre_context$ln" +					set pre_context {} +				} else { +					set m [expr $m+1] +					set patch "$patch$ln" +				} +			} elseif {$c1 ne {-} && $c1 ne {+}} { +				# context line +				set ln [$ui_diff get $i_l $next_l] +				set patch "$patch$pre_context$ln" +				# Skip the "\ No newline at end of +				# file". Depending on the locale setting +				# we don't know what this line looks +				# like exactly. The only thing we do +				# know is that it starts with "\ " +				if {![string match {\\ *} $ln]} { +					set n [expr $n+1] +					set m [expr $m+1] +				} +				set pre_context {} +			} elseif {$c1 eq $to_context} { +				# turn change line into context line +				set ln [$ui_diff get "$i_l + 1 chars" $next_l] +				if {$c1 eq {-}} { +					set pre_context "$pre_context $ln" +				} else { +					set patch "$patch $ln" +				} +				set n [expr $n+1] +				set m [expr $m+1] +			} else { +				# a change in the opposite direction of +				# to_context which is outside the range of +				# lines to apply. +				set patch "$patch$pre_context" +				set pre_context {} +			} +			set i_l $next_l +		} +		set patch "$patch$pre_context" +		set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch" +		set first_l [$ui_diff index "$next_l + 1 lines"] +	} + +	if {[catch { +		set enc [get_path_encoding $current_diff_path] +		set p [git_write $apply_cmd] +		fconfigure $p -translation binary -encoding $enc +		puts -nonewline $p $current_diff_header +		puts -nonewline $p $wholepatch +		close $p} err]} { +		error_popup "$failed_msg\n\n$err" +		unlock_index +		return +	} + +	if {$revert} { +		# Save a copy of this patch for undoing reverts. +		set last_revert $current_diff_header$wholepatch +		set last_revert_enc $enc +	} + +	unlock_index +} + +# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy +# of the diff applied is saved. Re-apply that diff to undo the revert. +# +# Right now, we only use a single variable to hold the copy, and not a +# stack/deque for simplicity, so multiple undos are not possible. Maybe this +# can be added if the need for something like this is felt in the future. +proc undo_last_revert {} { +	global last_revert current_diff_path current_diff_header +	global last_revert_enc + +	if {$last_revert eq {}} return +	if {![lock_index apply_hunk]} return + +	set apply_cmd {apply --whitespace=nowarn} +	set failed_msg [mc "Failed to undo last revert."] + +	if {[catch { +		set enc $last_revert_enc +		set p [git_write $apply_cmd] +		fconfigure $p -translation binary -encoding $enc +		puts -nonewline $p $last_revert +		close $p} err]} { +		error_popup "$failed_msg\n\n$err" +		unlock_index +		return +	} + +	set last_revert {} + +	unlock_index +} diff --git a/git-gui/lib/encoding.tcl b/git-gui/lib/encoding.tcl new file mode 100644 index 0000000000..d2e0fa60c3 --- /dev/null +++ b/git-gui/lib/encoding.tcl @@ -0,0 +1,466 @@ +# git-gui encoding support +# Copyright (C) 2005 Paul Mackerras <paulus@samba.org> +# (Copied from gitk, commit fd8ccbec4f0161) + +# This list of encoding names and aliases is distilled from +# https://www.iana.org/assignments/character-sets. +# Not all of them are supported by Tcl. +set encoding_aliases { +    { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII +      ISO646-US US-ASCII us IBM367 cp367 csASCII } +    { ISO-10646-UTF-1 csISO10646UTF1 } +    { ISO_646.basic:1983 ref csISO646basic1983 } +    { INVARIANT csINVARIANT } +    { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion } +    { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom } +    { NATS-SEFI iso-ir-8-1 csNATSSEFI } +    { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD } +    { NATS-DANO iso-ir-9-1 csNATSDANO } +    { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD } +    { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish } +    { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames } +    { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 } +    { ISO-2022-KR csISO2022KR } +    { EUC-KR csEUCKR } +    { ISO-2022-JP csISO2022JP } +    { ISO-2022-JP-2 csISO2022JP2 } +    { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7 +      csISO13JISC6220jp } +    { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro } +    { IT iso-ir-15 ISO646-IT csISO15Italian } +    { PT iso-ir-16 ISO646-PT csISO16Portuguese } +    { ES iso-ir-17 ISO646-ES csISO17Spanish } +    { greek7-old iso-ir-18 csISO18Greek7Old } +    { latin-greek iso-ir-19 csISO19LatinGreek } +    { DIN_66003 iso-ir-21 de ISO646-DE csISO21German } +    { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French } +    { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 } +    { ISO_5427 iso-ir-37 csISO5427Cyrillic } +    { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 } +    { BS_viewdata iso-ir-47 csISO47BSViewdata } +    { INIS iso-ir-49 csISO49INIS } +    { INIS-8 iso-ir-50 csISO50INIS8 } +    { INIS-cyrillic iso-ir-51 csISO51INISCyrillic } +    { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 } +    { ISO_5428:1980 iso-ir-55 csISO5428Greek } +    { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 } +    { GB_2312-80 iso-ir-58 chinese csISO58GB231280 } +    { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian +      csISO60Norwegian1 } +    { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 } +    { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French } +    { videotex-suppl iso-ir-70 csISO70VideotexSupp1 } +    { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 } +    { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 } +    { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian } +    { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 } +    { greek7 iso-ir-88 csISO88Greek7 } +    { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 } +    { iso-ir-90 csISO90 } +    { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a } +    { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b +      csISO92JISC62991984b } +    { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd } +    { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand } +    { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add +      csISO95JIS62291984handadd } +    { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana } +    { ISO_2033-1983 iso-ir-98 e13b csISO2033 } +    { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS } +    { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819 +      CP819 csISOLatin1 } +    { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 } +    { T.61-7bit iso-ir-102 csISO102T617bit } +    { T.61-8bit T.61 iso-ir-103 csISO103T618bit } +    { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 } +    { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 } +    { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic } +    { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 } +    { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 } +    { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr } +    { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708 +      arabic csISOLatinArabic } +    { ISO_8859-6-E csISO88596E ISO-8859-6-E } +    { ISO_8859-6-I csISO88596I ISO-8859-6-I } +    { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118 +      greek greek8 csISOLatinGreek } +    { T.101-G2 iso-ir-128 csISO128T101G2 } +    { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew +      csISOLatinHebrew } +    { ISO_8859-8-E csISO88598E ISO-8859-8-E } +    { ISO_8859-8-I csISO88598I ISO-8859-8-I } +    { CSN_369103 iso-ir-139 csISO139CSN369103 } +    { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 } +    { ISO_6937-2-add iso-ir-142 csISOTextComm } +    { IEC_P27-1 iso-ir-143 csISO143IECP271 } +    { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic +      csISOLatinCyrillic } +    { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian } +    { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian } +    { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 } +    { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT } +    { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba } +    { ISO_6937-2-25 iso-ir-152 csISO6937Add } +    { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 } +    { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp } +    { ISO_10367-box iso-ir-155 csISO10367Box } +    { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 } +    { latin-lap lap iso-ir-158 csISO158Lap } +    { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 } +    { DS_2089 DS2089 ISO646-DK dk csISO646Danish } +    { us-dk csUSDK } +    { dk-us csDKUS } +    { JIS_X0201 X0201 csHalfWidthKatakana } +    { KSC5636 ISO646-KR csKSC5636 } +    { ISO-10646-UCS-2 csUnicode } +    { ISO-10646-UCS-4 csUCS4 } +    { DEC-MCS dec csDECMCS } +    { hp-roman8 roman8 r8 csHPRoman8 } +    { macintosh mac csMacintosh } +    { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl +      csIBM037 } +    { IBM038 EBCDIC-INT cp038 csIBM038 } +    { IBM273 CP273 csIBM273 } +    { IBM274 EBCDIC-BE CP274 csIBM274 } +    { IBM275 EBCDIC-BR cp275 csIBM275 } +    { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 } +    { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 } +    { IBM280 CP280 ebcdic-cp-it csIBM280 } +    { IBM281 EBCDIC-JP-E cp281 csIBM281 } +    { IBM284 CP284 ebcdic-cp-es csIBM284 } +    { IBM285 CP285 ebcdic-cp-gb csIBM285 } +    { IBM290 cp290 EBCDIC-JP-kana csIBM290 } +    { IBM297 cp297 ebcdic-cp-fr csIBM297 } +    { IBM420 cp420 ebcdic-cp-ar1 csIBM420 } +    { IBM423 cp423 ebcdic-cp-gr csIBM423 } +    { IBM424 cp424 ebcdic-cp-he csIBM424 } +    { IBM437 cp437 437 csPC8CodePage437 } +    { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 } +    { IBM775 cp775 csPC775Baltic } +    { IBM850 cp850 850 csPC850Multilingual } +    { IBM851 cp851 851 csIBM851 } +    { IBM852 cp852 852 csPCp852 } +    { IBM855 cp855 855 csIBM855 } +    { IBM857 cp857 857 csIBM857 } +    { IBM860 cp860 860 csIBM860 } +    { IBM861 cp861 861 cp-is csIBM861 } +    { IBM862 cp862 862 csPC862LatinHebrew } +    { IBM863 cp863 863 csIBM863 } +    { IBM864 cp864 csIBM864 } +    { IBM865 cp865 865 csIBM865 } +    { IBM866 cp866 866 csIBM866 } +    { IBM868 CP868 cp-ar csIBM868 } +    { IBM869 cp869 869 cp-gr csIBM869 } +    { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 } +    { IBM871 CP871 ebcdic-cp-is csIBM871 } +    { IBM880 cp880 EBCDIC-Cyrillic csIBM880 } +    { IBM891 cp891 csIBM891 } +    { IBM903 cp903 csIBM903 } +    { IBM904 cp904 904 csIBBM904 } +    { IBM905 CP905 ebcdic-cp-tr csIBM905 } +    { IBM918 CP918 ebcdic-cp-ar2 csIBM918 } +    { IBM1026 CP1026 csIBM1026 } +    { EBCDIC-AT-DE csIBMEBCDICATDE } +    { EBCDIC-AT-DE-A csEBCDICATDEA } +    { EBCDIC-CA-FR csEBCDICCAFR } +    { EBCDIC-DK-NO csEBCDICDKNO } +    { EBCDIC-DK-NO-A csEBCDICDKNOA } +    { EBCDIC-FI-SE csEBCDICFISE } +    { EBCDIC-FI-SE-A csEBCDICFISEA } +    { EBCDIC-FR csEBCDICFR } +    { EBCDIC-IT csEBCDICIT } +    { EBCDIC-PT csEBCDICPT } +    { EBCDIC-ES csEBCDICES } +    { EBCDIC-ES-A csEBCDICESA } +    { EBCDIC-ES-S csEBCDICESS } +    { EBCDIC-UK csEBCDICUK } +    { EBCDIC-US csEBCDICUS } +    { UNKNOWN-8BIT csUnknown8BiT } +    { MNEMONIC csMnemonic } +    { MNEM csMnem } +    { VISCII csVISCII } +    { VIQR csVIQR } +    { KOI8-R csKOI8R } +    { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro } +    { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro } +    { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro } +    { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro } +    { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro } +    { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro } +    { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro } +    { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro } +    { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro } +    { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro } +    { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro } +    { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro } +    { IBM1047 IBM-1047 } +    { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian } +    { Amiga-1251 Ami1251 Amiga1251 Ami-1251 } +    { UNICODE-1-1 csUnicode11 } +    { CESU-8 csCESU-8 } +    { BOCU-1 csBOCU-1 } +    { UNICODE-1-1-UTF-7 csUnicode11UTF7 } +    { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic +      l8 } +    { ISO-8859-15 ISO_8859-15 Latin-9 } +    { 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 ShiftJIS Shift-JIS } +    { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese +      EUC-JP } +    { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese } +    { ISO-10646-UCS-Basic csUnicodeASCII } +    { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 } +    { ISO-Unicode-IBM-1261 csUnicodeIBM1261 } +    { ISO-Unicode-IBM-1268 csUnicodeIBM1268 } +    { ISO-Unicode-IBM-1276 csUnicodeIBM1276 } +    { ISO-Unicode-IBM-1264 csUnicodeIBM1264 } +    { ISO-Unicode-IBM-1265 csUnicodeIBM1265 } +    { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 } +    { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 } +    { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 } +    { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 } +    { Adobe-Standard-Encoding csAdobeStandardEncoding } +    { Ventura-US csVenturaUS } +    { Ventura-International csVenturaInternational } +    { PC8-Danish-Norwegian csPC8DanishNorwegian } +    { PC8-Turkish csPC8Turkish } +    { IBM-Symbols csIBMSymbols } +    { IBM-Thai csIBMThai } +    { HP-Legal csHPLegal } +    { HP-Pi-font csHPPiFont } +    { HP-Math8 csHPMath8 } +    { Adobe-Symbol-Encoding csHPPSMath } +    { HP-DeskTop csHPDesktop } +    { Ventura-Math csVenturaMath } +    { Microsoft-Publishing csMicrosoftPublishing } +    { Windows-31J csWindows31J } +    { GB2312 csGB2312 } +    { 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_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 {$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 +		} +	} +	build_encoding_submenu $emenu [list other [mc Other] $unused_grp] $cmd +} diff --git a/git-gui/lib/error.tcl b/git-gui/lib/error.tcl new file mode 100644 index 0000000000..fc0b5ad5e0 --- /dev/null +++ b/git-gui/lib/error.tcl @@ -0,0 +1,118 @@ +# git-gui branch (create/delete) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc _error_parent {} { +	set p [grab current .] +	if {$p eq {}} { +		return . +	} +	return $p +} + +proc error_popup {msg} { +	set title [appname] +	if {[reponame] ne {}} { +		append title " ([reponame])" +	} +	set cmd [list tk_messageBox \ +		-icon error \ +		-type ok \ +		-title [mc "%s: error" $title] \ +		-message $msg] +	if {[winfo ismapped [_error_parent]]} { +		lappend cmd -parent [_error_parent] +	} +	eval $cmd +} + +proc warn_popup {msg} { +	set title [appname] +	if {[reponame] ne {}} { +		append title " ([reponame])" +	} +	set cmd [list tk_messageBox \ +		-icon warning \ +		-type ok \ +		-title [mc "%s: warning" $title] \ +		-message $msg] +	if {[winfo ismapped [_error_parent]]} { +		lappend cmd -parent [_error_parent] +	} +	eval $cmd +} + +proc info_popup {msg} { +	set title [appname] +	if {[reponame] ne {}} { +		append title " ([reponame])" +	} +	tk_messageBox \ +		-parent [_error_parent] \ +		-icon info \ +		-type ok \ +		-title $title \ +		-message $msg +} + +proc ask_popup {msg} { +	set title [appname] +	if {[reponame] ne {}} { +		append title " ([reponame])" +	} +	set cmd [list tk_messageBox \ +		-icon question \ +		-type yesno \ +		-title $title \ +		-message $msg] +	if {[winfo ismapped [_error_parent]]} { +		lappend cmd -parent [_error_parent] +	} +	eval $cmd +} + +proc hook_failed_popup {hook msg {is_fatal 1}} { +	set w .hookfail +	Dialog $w +	wm withdraw $w + +	ttk::frame $w.m +	ttk::label $w.m.l1 -text [mc "%s hook failed:" $hook] \ +		-anchor w \ +		-justify left \ +		-font font_uibold +	text $w.m.t \ +		-background white \ +		-foreground black \ +		-borderwidth 1 \ +		-relief sunken \ +		-width 80 -height 10 \ +		-font font_diff \ +		-yscrollcommand [list $w.m.sby set] +	ttk::scrollbar $w.m.sby -command [list $w.m.t yview] +	pack $w.m.l1 -side top -fill x +	if {$is_fatal} { +		ttk::label $w.m.l2 \ +			-text [mc "You must correct the above errors before committing."] \ +			-anchor w \ +			-justify left \ +			-font font_uibold +		pack $w.m.l2 -side bottom -fill x +	} +	pack $w.m.sby -side right -fill y +	pack $w.m.t -side left -fill both -expand 1 +	pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10 + +	$w.m.t insert 1.0 $msg +	$w.m.t conf -state disabled + +	ttk::button $w.ok -text OK \ +		-width 15 \ +		-command "destroy $w" +	pack $w.ok -side bottom -anchor e -pady 10 -padx 10 + +	bind $w <Visibility> "grab $w; focus $w" +	bind $w <Key-Return> "destroy $w" +	wm title $w [mc "%s (%s): error" [appname] [reponame]] +	wm deiconify $w +	tkwait window $w +} diff --git a/git-gui/lib/git-gui.ico b/git-gui/lib/git-gui.icoBinary files differ new file mode 100644 index 0000000000..334cfa5a1a --- /dev/null +++ b/git-gui/lib/git-gui.ico diff --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl new file mode 100644 index 0000000000..e1d38e54be --- /dev/null +++ b/git-gui/lib/index.tcl @@ -0,0 +1,753 @@ +# git-gui index (add/remove) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc _delete_indexlock {} { +	if {[catch {file delete -- [gitdir index.lock]} err]} { +		error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"] +	} +} + +proc close_and_unlock_index {fd after} { +	if {![catch {_close_updateindex $fd} err]} { +		unlock_index +		uplevel #0 $after +	} else { +		rescan_on_error $err $after +	} +} + +proc _close_updateindex {fd} { +	fconfigure $fd -blocking 1 +	close $fd +} + +proc rescan_on_error {err {after {}}} { +	set w .indexfried +	Dialog $w +	wm withdraw $w +	wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]] +	wm geometry $w "+[winfo rootx .]+[winfo rooty .]" +	set s [mc "Updating the Git index failed.  A rescan will be automatically started to resynchronize git-gui."] +	text $w.msg -yscrollcommand [list $w.vs set] \ +		-width [string length $s] -relief flat \ +		-borderwidth 0 -highlightthickness 0 \ +		-background [get_bg_color $w] +	$w.msg tag configure bold -font font_uibold -justify center +	ttk::scrollbar $w.vs -command [list $w.msg yview] +	$w.msg insert end $s bold \n\n$err {} +	$w.msg configure -state disabled + +	ttk::button $w.continue \ +		-text [mc "Continue"] \ +		-command [list destroy $w] +	ttk::button $w.unlock \ +		-text [mc "Unlock Index"] \ +		-command "destroy $w; _delete_indexlock" +	grid $w.msg - $w.vs -sticky news +	grid $w.unlock $w.continue - -sticky se -padx 2 -pady 2 +	grid columnconfigure $w 0 -weight 1 +	grid rowconfigure $w 0 -weight 1 + +	wm protocol $w WM_DELETE_WINDOW update +	bind $w.continue <Visibility> " +		grab $w +		focus %W +	" +	wm deiconify $w +	tkwait window $w + +	$::main_status stop_all +	unlock_index +	rescan [concat $after {ui_ready;}] 0 +} + +proc update_indexinfo {msg path_list after} { +	global update_index_cp + +	if {![lock_index update]} return + +	set update_index_cp 0 +	set path_list [lsort $path_list] +	set total_cnt [llength $path_list] +	set batch [expr {int($total_cnt * .01) + 1}] +	if {$batch > 25} {set batch 25} + +	set status_bar_operation [$::main_status start $msg [mc "files"]] +	set fd [git_write [list update-index -z --index-info]] +	fconfigure $fd \ +		-blocking 0 \ +		-buffering full \ +		-buffersize 512 \ +		-translation binary +	fileevent $fd writable [list \ +		write_update_indexinfo \ +		$fd \ +		$path_list \ +		$total_cnt \ +		$batch \ +		$status_bar_operation \ +		$after \ +		] +} + +proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \ +	after} { +	global update_index_cp +	global file_states current_diff_path + +	if {$update_index_cp >= $total_cnt} { +		$status_bar_operation stop +		close_and_unlock_index $fd $after +		return +	} + +	for {set i $batch} \ +		{$update_index_cp < $total_cnt && $i > 0} \ +		{incr i -1} { +		set path [lindex $path_list $update_index_cp] +		incr update_index_cp + +		set s $file_states($path) +		switch -glob -- [lindex $s 0] { +		A? {set new _O} +		MT - +		TM - +		T_ {set new _T} +		M? {set new _M} +		TD - +		D_ {set new _D} +		D? {set new _?} +		?? {continue} +		} +		set info [lindex $s 2] +		if {$info eq {}} continue + +		puts -nonewline $fd "$info\t[encoding convertto utf-8 $path]\0" +		display_file $path $new +	} + +	$status_bar_operation update $update_index_cp $total_cnt +} + +proc update_index {msg path_list after} { +	global update_index_cp + +	if {![lock_index update]} return + +	set update_index_cp 0 +	set path_list [lsort $path_list] +	set total_cnt [llength $path_list] +	set batch [expr {int($total_cnt * .01) + 1}] +	if {$batch > 25} {set batch 25} + +	set status_bar_operation [$::main_status start $msg [mc "files"]] +	set fd [git_write [list update-index --add --remove -z --stdin]] +	fconfigure $fd \ +		-blocking 0 \ +		-buffering full \ +		-buffersize 512 \ +		-translation binary +	fileevent $fd writable [list \ +		write_update_index \ +		$fd \ +		$path_list \ +		$total_cnt \ +		$batch \ +		$status_bar_operation \ +		$after \ +		] +} + +proc write_update_index {fd path_list total_cnt batch status_bar_operation \ +	after} { +	global update_index_cp +	global file_states current_diff_path + +	if {$update_index_cp >= $total_cnt} { +		$status_bar_operation stop +		close_and_unlock_index $fd $after +		return +	} + +	for {set i $batch} \ +		{$update_index_cp < $total_cnt && $i > 0} \ +		{incr i -1} { +		set path [lindex $path_list $update_index_cp] +		incr update_index_cp + +		switch -glob -- [lindex $file_states($path) 0] { +		AD {set new __} +		?D {set new D_} +		_O - +		AT - +		AM {set new A_} +		TM - +		MT - +		_T {set new T_} +		_U - +		U? { +			if {[file exists $path]} { +				set new M_ +			} else { +				set new D_ +			} +		} +		?M {set new M_} +		?? {continue} +		} +		puts -nonewline $fd "[encoding convertto utf-8 $path]\0" +		display_file $path $new +	} + +	$status_bar_operation update $update_index_cp $total_cnt +} + +proc checkout_index {msg path_list after capture_error} { +	global update_index_cp + +	if {![lock_index update]} return + +	set update_index_cp 0 +	set path_list [lsort $path_list] +	set total_cnt [llength $path_list] +	set batch [expr {int($total_cnt * .01) + 1}] +	if {$batch > 25} {set batch 25} + +	set status_bar_operation [$::main_status start $msg [mc "files"]] +	set fd [git_write [list checkout-index \ +		--index \ +		--quiet \ +		--force \ +		-z \ +		--stdin \ +		]] +	fconfigure $fd \ +		-blocking 0 \ +		-buffering full \ +		-buffersize 512 \ +		-translation binary +	fileevent $fd writable [list \ +		write_checkout_index \ +		$fd \ +		$path_list \ +		$total_cnt \ +		$batch \ +		$status_bar_operation \ +		$after \ +		$capture_error \ +		] +} + +proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \ +	after capture_error} { +	global update_index_cp +	global file_states current_diff_path + +	if {$update_index_cp >= $total_cnt} { +		$status_bar_operation stop + +		# We do not unlock the index directly here because this +		# operation expects to potentially run in parallel with file +		# deletions scheduled by revert_helper. We're done with the +		# update index, so we close it, but actually unlocking the index +		# and dealing with potential errors is deferred to the chord +		# body that runs when all async operations are completed. +		# +		# (See after_chord in revert_helper.) + +		if {[catch {_close_updateindex $fd} err]} { +			uplevel #0 $capture_error [list $err] +		} + +		uplevel #0 $after + +		return +	} + +	for {set i $batch} \ +		{$update_index_cp < $total_cnt && $i > 0} \ +		{incr i -1} { +		set path [lindex $path_list $update_index_cp] +		incr update_index_cp +		switch -glob -- [lindex $file_states($path) 0] { +		U? {continue} +		?M - +		?T - +		?D { +			puts -nonewline $fd "[encoding convertto utf-8 $path]\0" +			display_file $path ?_ +		} +		} +	} + +	$status_bar_operation update $update_index_cp $total_cnt +} + +proc unstage_helper {txt paths} { +	global file_states current_diff_path + +	if {![lock_index begin-update]} return + +	set path_list [list] +	set after {} +	foreach path $paths { +		switch -glob -- [lindex $file_states($path) 0] { +		A? - +		M? - +		T? - +		D? { +			lappend path_list $path +			if {$path eq $current_diff_path} { +				set after {reshow_diff;} +			} +		} +		} +	} +	if {$path_list eq {}} { +		unlock_index +	} else { +		update_indexinfo \ +			$txt \ +			$path_list \ +			[concat $after {ui_ready;}] +	} +} + +proc do_unstage_selection {} { +	global current_diff_path selected_paths + +	if {[array size selected_paths] > 0} { +		unstage_helper \ +			[mc "Unstaging selected files from commit"] \ +			[array names selected_paths] +	} elseif {$current_diff_path ne {}} { +		unstage_helper \ +			[mc "Unstaging %s from commit" [short_path $current_diff_path]] \ +			[list $current_diff_path] +	} +} + +proc add_helper {txt paths} { +	global file_states current_diff_path + +	if {![lock_index begin-update]} return + +	set path_list [list] +	set after {} +	foreach path $paths { +		switch -glob -- [lindex $file_states($path) 0] { +		_U - +		U? { +			if {$path eq $current_diff_path} { +				unlock_index +				merge_stage_workdir $path +				return +			} +		} +		_O - +		?M - +		?D - +		?T { +			lappend path_list $path +			if {$path eq $current_diff_path} { +				set after {reshow_diff;} +			} +		} +		} +	} +	if {$path_list eq {}} { +		unlock_index +	} else { +		update_index \ +			$txt \ +			$path_list \ +			[concat $after {ui_status [mc "Ready to commit."];}] +	} +} + +proc do_add_selection {} { +	global current_diff_path selected_paths + +	if {[array size selected_paths] > 0} { +		add_helper \ +			[mc "Adding selected files"] \ +			[array names selected_paths] +	} elseif {$current_diff_path ne {}} { +		add_helper \ +			[mc "Adding %s" [short_path $current_diff_path]] \ +			[list $current_diff_path] +	} +} + +proc do_add_all {} { +	global file_states + +	set paths [list] +	set untracked_paths [list] +	foreach path [array names file_states] { +		switch -glob -- [lindex $file_states($path) 0] { +		U? {continue} +		?M - +		?T - +		?D {lappend paths $path} +		?O {lappend untracked_paths $path} +		} +	} +	if {[llength $untracked_paths]} { +		set reply 0 +		switch -- [get_config gui.stageuntracked] { +		no { +			set reply 0 +		} +		yes { +			set reply 1 +		} +		ask - +		default { +			set reply [ask_popup [mc "Stage %d untracked files?" \ +									  [llength $untracked_paths]]] +		} +		} +		if {$reply} { +			set paths [concat $paths $untracked_paths] +		} +	} +	add_helper [mc "Adding all changed files"] $paths +} + +# Copied from TclLib package "lambda". +proc lambda {arguments body args} { +	return [list ::apply [list $arguments $body] {*}$args] +} + +proc revert_helper {txt paths} { +	global file_states current_diff_path + +	if {![lock_index begin-update]} return + +	# Workaround for Tcl < 9.0: chord namespaces are not obeyed and +	# operated in the global namespace. This clears an error that could +	# have been left over from a previous operation. +	set ::err {} + +	# Common "after" functionality that waits until multiple asynchronous +	# operations are complete (by waiting for them to activate their notes +	# on the chord). +	# +	# The asynchronous operations are each indicated below by a comment +	# before the code block that starts the async operation. +	set after_chord [SimpleChord::new { +		if {[info exists err] && [string trim $err] ne ""} { +			rescan_on_error $err +		} else { +			unlock_index +			if {$should_reshow_diff} { reshow_diff } +			ui_ready +		} +	}] + +	$after_chord eval { set should_reshow_diff 0 } + +	# This function captures an error for processing when after_chord is +	# completed. (The chord is curried into the lambda function.) +	set capture_error [lambda \ +		{chord error} \ +		{ $chord eval [list set err $error] } \ +		$after_chord] + +	# We don't know how many notes we're going to create (it's dynamic based +	# on conditional paths below), so create a common note that will delay +	# the chord's completion until we activate it, and then activate it +	# after all the other notes have been created. +	set after_common_note [$after_chord add_note] + +	set path_list [list] +	set untracked_list [list] + +	foreach path $paths { +		switch -glob -- [lindex $file_states($path) 0] { +		U? {continue} +		?O { +			lappend untracked_list $path +		} +		?M - +		?T - +		?D { +			lappend path_list $path +			if {$path eq $current_diff_path} { +				$after_chord eval { set should_reshow_diff 1 } +			} +		} +		} +	} + +	set path_cnt [llength $path_list] +	set untracked_cnt [llength $untracked_list] + +	# Asynchronous operation: revert changes by checking them out afresh +	# from the index. +	if {$path_cnt > 0} { +		# Split question between singular and plural cases, because +		# such distinction is needed in some languages. Previously, the +		# code used "Revert changes in" for both, but that can't work +		# in languages where 'in' must be combined with word from +		# rest of string (in different way for both cases of course). +		# +		# FIXME: Unfortunately, even that isn't enough in some languages +		# as they have quite complex plural-form rules. Unfortunately, +		# msgcat doesn't seem to support that kind of string +		# translation. +		# +		if {$path_cnt == 1} { +			set query [mc \ +				"Revert changes in file %s?" \ +				[short_path [lindex $path_list]] \ +				] +		} else { +			set query [mc \ +				"Revert changes in these %i files?" \ +				$path_cnt] +		} + +		set reply [tk_dialog \ +			.confirm_revert \ +			"[appname] ([reponame])" \ +			"$query + +[mc "Any unstaged changes will be permanently lost by the revert."]" \ +			question \ +			1 \ +			[mc "Do Nothing"] \ +			[mc "Revert Changes"] \ +			] + +		if {$reply == 1} { +			set note [$after_chord add_note] +			checkout_index \ +				$txt \ +				$path_list \ +				[list $note activate] \ +				$capture_error +		} +	} + +	# Asynchronous operation: Deletion of untracked files. +	if {$untracked_cnt > 0} { +		# Split question between singular and plural cases, because +		# such distinction is needed in some languages. +		# +		# FIXME: Unfortunately, even that isn't enough in some languages +		# as they have quite complex plural-form rules. Unfortunately, +		# msgcat doesn't seem to support that kind of string +		# translation. +		# +		if {$untracked_cnt == 1} { +			set query [mc \ +				"Delete untracked file %s?" \ +				[short_path [lindex $untracked_list]] \ +				] +		} else { +			set query [mc \ +				"Delete these %i untracked files?" \ +				$untracked_cnt \ +				] +		} + +		set reply [tk_dialog \ +			.confirm_revert \ +			"[appname] ([reponame])" \ +			"$query + +[mc "Files will be permanently deleted."]" \ +			question \ +			1 \ +			[mc "Do Nothing"] \ +			[mc "Delete Files"] \ +			] + +		if {$reply == 1} { +			$after_chord eval { set should_reshow_diff 1 } + +			set note [$after_chord add_note] +			delete_files $untracked_list [list $note activate] +		} +	} + +	# Activate the common note. If no other notes were created, this +	# completes the chord. If other notes were created, then this common +	# note prevents a race condition where the chord might complete early. +	$after_common_note activate +} + +# Delete all of the specified files, performing deletion in batches to allow the +# UI to remain responsive and updated. +proc delete_files {path_list after} { +	# Enable progress bar status updates +	set status_bar_operation [$::main_status \ +		start \ +		[mc "Deleting"] \ +		[mc "files"]] + +	set path_index 0 +	set deletion_errors [list] +	set batch_size 50 + +	delete_helper \ +		$path_list \ +		$path_index \ +		$deletion_errors \ +		$batch_size \ +		$status_bar_operation \ +		$after +} + +# Helper function to delete a list of files in batches. Each call deletes one +# batch of files, and then schedules a call for the next batch after any UI +# messages have been processed. +proc delete_helper {path_list path_index deletion_errors batch_size \ +	status_bar_operation after} { +	global file_states + +	set path_cnt [llength $path_list] + +	set batch_remaining $batch_size + +	while {$batch_remaining > 0} { +		if {$path_index >= $path_cnt} { break } + +		set path [lindex $path_list $path_index] + +		set deletion_failed [catch {file delete -- $path} deletion_error] + +		if {$deletion_failed} { +			lappend deletion_errors [list "$deletion_error"] +		} else { +			remove_empty_directories [file dirname $path] + +			# Don't assume the deletion worked. Remove the file from +			# the UI, but only if it no longer exists. +			if {![path_exists $path]} { +				unset file_states($path) +				display_file $path __ +			} +		} + +		incr path_index 1 +		incr batch_remaining -1 +	} + +	# Update the progress bar to indicate that this batch has been +	# completed. The update will be visible when this procedure returns +	# and allows the UI thread to process messages. +	$status_bar_operation update $path_index $path_cnt + +	if {$path_index < $path_cnt} { +		# The Tcler's Wiki lists this as the best practice for keeping +		# a UI active and processing messages during a long-running +		# operation. + +		after idle [list after 0 [list \ +			delete_helper \ +			$path_list \ +			$path_index \ +			$deletion_errors \ +			$batch_size \ +			$status_bar_operation \ +			$after +			]] +	} else { +		# Finish the status bar operation. +		$status_bar_operation stop + +		# Report error, if any, based on how many deletions failed. +		set deletion_error_cnt [llength $deletion_errors] + +		if {($deletion_error_cnt > 0) +		 && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} { +			set error_text [mc "Encountered errors deleting files:\n"] + +			foreach deletion_error $deletion_errors { +				append error_text "* [lindex $deletion_error 0]\n" +			} + +			error_popup $error_text +		} elseif {$deletion_error_cnt == $path_cnt} { +			error_popup [mc \ +				"None of the %d selected files could be deleted." \ +				$path_cnt \ +				] +		} elseif {$deletion_error_cnt > 1} { +			error_popup [mc \ +				"%d of the %d selected files could not be deleted." \ +				$deletion_error_cnt \ +				$path_cnt \ +				] +		} + +		uplevel #0 $after +	} +} + +proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; } + +# This function is from the TCL documentation: +# +#   https://wiki.tcl-lang.org/page/file+exists +# +# [file exists] returns false if the path does exist but is a symlink to a path +# that doesn't exist. This proc returns true if the path exists, regardless of +# whether it is a symlink and whether it is broken. +proc path_exists {name} { +	expr {![catch {file lstat $name finfo}]} +} + +# Remove as many empty directories as we can starting at the specified path, +# walking up the directory tree. If we encounter a directory that is not +# empty, or if a directory deletion fails, then we stop the operation and +# return to the caller. Even if this procedure fails to delete any +# directories at all, it does not report failure. +proc remove_empty_directories {directory_path} { +	set parent_path [file dirname $directory_path] + +	while {$parent_path != $directory_path} { +		set contents [glob -nocomplain -dir $directory_path *] + +		if {[llength $contents] > 0} { break } +		if {[catch {file delete -- $directory_path}]} { break } + +		set directory_path $parent_path +		set parent_path [file dirname $directory_path] +	} +} + +proc do_revert_selection {} { +	global current_diff_path selected_paths + +	if {[array size selected_paths] > 0} { +		revert_helper \ +			[mc "Reverting selected files"] \ +			[array names selected_paths] +	} elseif {$current_diff_path ne {}} { +		revert_helper \ +			[mc "Reverting %s" [short_path $current_diff_path]] \ +			[list $current_diff_path] +	} +} + +proc do_select_commit_type {} { +	global commit_type commit_type_is_amend + +	if {$commit_type_is_amend == 0 +		&& [string match amend* $commit_type]} { +		create_new_commit +	} elseif {$commit_type_is_amend == 1 +		&& ![string match amend* $commit_type]} { +		load_last_commit + +		# The amend request was rejected... +		# +		if {![string match amend* $commit_type]} { +			set commit_type_is_amend 0 +		} +	} +} diff --git a/git-gui/lib/line.tcl b/git-gui/lib/line.tcl new file mode 100644 index 0000000000..5980ae805c --- /dev/null +++ b/git-gui/lib/line.tcl @@ -0,0 +1,80 @@ +# goto line number +# based on code from gitk, Copyright (C) Paul Mackerras + +class linebar { + +field w +field ctext + +field linenum   {} + +constructor new {i_w i_text args} { +	set w      $i_w +	set ctext  $i_text + +	ttk::frame  $w +	ttk::label  $w.l       -text [mc "Goto Line:"] +	tentry  $w.ent \ +		-textvariable ${__this}::linenum \ +		-background lightgreen \ +		-validate key \ +		-validatecommand [cb _validate %P] +	ttk::button $w.bn      -text [mc Go] -command [cb _goto] + +	pack   $w.l   -side left +	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 linenum write [cb _goto_cb] +	bind $w.ent <Return> [cb _goto] +	bind $w.ent <Escape> [cb hide] + +	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]} { +		$w.ent delete 0 end +		focus $ctext +		grid remove $w +	} +} + +method visible {} { +	return [winfo ismapped $w] +} + +method editor {} { +	return $w.ent +} + +method _validate {P} { +	# only accept numbers as input +	string is integer $P +} + +method _goto_cb {name ix op} { +	after idle [cb _goto 1] +} + +method _goto {{nohide {0}}} { +	if {$linenum ne {}} { +		$ctext see $linenum.0 +		if {!$nohide} { +			hide $this +		} +	} +} + +} diff --git a/git-gui/lib/logo.tcl b/git-gui/lib/logo.tcl new file mode 100644 index 0000000000..5ff76692f5 --- /dev/null +++ b/git-gui/lib/logo.tcl @@ -0,0 +1,43 @@ +# git-gui Git Gui logo +# Copyright (C) 2007 Shawn Pearce + +# Henrik Nyh's alternative Git logo, from his blog post +# http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon +# +image create photo ::git_logo_data -data { +R0lGODdhYQC8AIQbAGZmZtg4LW9vb3l5eYKCgoyMjEC/TOJpYZWVlZ+fn2/PeKmpqbKysry8vMXF +xZ/fpc/Pz7fnvPXNytnZ2eLi4s/v0vja1+zs7Of36fX19f3z8v///////////////////ywAAAAA +YQC8AAAF/uAmjmRpnmiqrmzrvq4hz3RtGw+s7zx5/7dcb0hUAY8zYXHJRCKVzGjPeYRKry8q0Irt +GrVBr3gFDo/PprKNix6ra+y2902Ly7H05L2dl9n3UX04gGeCf4RFhohiiotdjY5XkJGBfYeUOpOY +iZablXmXURgPpKWmp6ipqYIKqq6vqREjFYK1trUKs7e7vFq5IrS9wsM0vxvBxMm8xsjKzqy6z9J5 +zNPWatXX2k7Z29433d/iMuHj3+Xm2+jp1+vs0+7vz/HyyvT1xPf4wvr7y9H+pBkbBasgLFYGE8ba +o8nTlE4OOYGKKJFOKIopGmLMAnHjDo0eWYAM+WUiSRgj/k+eSKmyBMuWI17C3CATZs2WN1XmPLmT +ZM+QPz0G3VihqNGjSJNWwDCzqdOnUKPu0SChqtWrWLNq3cq1q9evYCVYGCEhgNmzaNOqXcu2rdu3 +cOMGOEBWrt27ePPCpSuirN6/gAO35bvBr+DDiPMSNpy4sWO2ix9Lnmw2MuXLiS1j3gxYM+fPdz2D +Hv1WNOnTak2jXj23LuvXlV3DZq16Nujatjnjzo15N2/Kvn9LDi7cMfHimaUqX868ufPn0KPPpOCA +AQMWCQBo3869u/fv4MNrd3DlQoMC3QlkSJFdvPv38LVDWJLBAYHwE1LE38+/+/UhGTAggHv5odDf +gfv9/seDgPAVeAKCELqnIAwU3BefgyZEqOF3E7rAQH8YlrDhiNt1uEIG6IGoH4kjmpjCBRaqaCCL +G7p4AgUDIhgiCTTW2AKOEe44Qo8a2khCBgNoKKQIREZopAgZxAjhkhs0CeGTG7Sn5IpW9vekAyRS +2eWBRl6Q44ZijhlfAQlQmeKIaarpHZsMTHABCxDQGKec3JH3QpIs7snndn6yAKaeXA7aZwuABppo +fAws0GiEhaKQJ40F3DkjfwVC8CaCAlCgAgIkJjDfCgdiOMGn/Q2w3gkZtPgqC6ma0ECECaBwa4QE +aOpCrSYAqeMJpEKYqw7ABnsmfwQ8aCwPySqLYKUb/kwAYbPQyoiCtQcOUMKHBwrgK7LaogBuuaxC +OkS0KEwa37EiLBufALPuwO4Jh/InwAixkknEvSe4C9+p3PY3rr3lpnDufguIcCmzRQAc7IHYLhxf +w/8mnILA74lg8cARa4xCsZxusMCBomZccgsfv0deuh2HvLKh/sLs3hJSvieuCwUzvIHN4tGXc3ih +vtDzmj8fSNLR8BWQdH9LH+g00OFF3d/UBx4cUcvuOc21eFRiouV+Xvvr0dDvlX21R/2uzTR89TqU +L3+5UoBgAxtRHd5/CHpLkd13i4D2e3hHRLKMY+9Hr0Nvx/fq3Pw57cng7/m9wQVObnIyhAiQwHF8 +/tQS8nDgI2wOYeh3CAvhuIBHiDEgqvdtwudkaz3GBPKaTcKuGgqAJRMZmK6h1hnk3ncDcUvhgPFS +o5B476ZKQcECzCN4qgmYN4lAncmzcAEEkhJp+QlfkyhAAdtbN8H67FvHQAF6b4g6v9UryqfkKkBu +v/0prxD//kR63YnqB8AeqcdoBRxU/1zAuwRaaX4reJ4DSSRAHUhwgrgqwgUx2B94EWGDHISPBzUY +QgSNcAn6K6F4fscDCtBOhdoRwPW6kIHDwZA7vWoDBF44Qd/tIUAEBCACbIeG4AXxfmFrQ4B4OCYE +JBEQELChmgbAACJioj4JOCKCCLCABZ6EAg1IHwDlyLYAB1gRJhSYgHUQAD9WnQ9+CWBAA+wknTpC +JwQAOw== +} + +proc git_logo {w} { +	label $w \ +		-borderwidth 1 \ +		-relief sunken \ +		-background white \ +		-image ::git_logo_data +	return $w +} diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl new file mode 100644 index 0000000000..3490beddae --- /dev/null +++ b/git-gui/lib/merge.tcl @@ -0,0 +1,276 @@ +# git-gui branch merge support +# Copyright (C) 2006, 2007 Shawn Pearce + +class merge { + +field w         ; # top level window +field w_rev     ; # mega-widget to pick the revision to merge + +method _can_merge {} { +	global HEAD commit_type file_states + +	if {[string match amend* $commit_type]} { +		info_popup [mc "Cannot merge while amending. + +You must finish amending this commit before starting any type of merge. +"] +		return 0 +	} + +	if {[committer_ident] eq {}} {return 0} +	if {![lock_index merge]} {return 0} + +	# -- Our in memory state should match the repository. +	# +	repository_state curType curHEAD curMERGE_HEAD +	if {$commit_type ne $curType || $HEAD ne $curHEAD} { +		info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan.  A rescan must be performed before a merge can be performed. + +The rescan will be automatically started now. +"] +		unlock_index +		rescan ui_ready +		return 0 +	} + +	foreach path [array names file_states] { +		switch -glob -- [lindex $file_states($path) 0] { +		_O { +			continue; # and pray it works! +		} +		_U - +		U? { +			error_popup [mc "You are in the middle of a conflicted merge. + +File %s has merge conflicts. + +You must resolve them, stage the file, and commit to complete the current merge.  Only then can you begin another merge. +" [short_path $path]] +			unlock_index +			return 0 +		} +		?? { +			error_popup [mc "You are in the middle of a change. + +File %s is modified. + +You should complete the current commit before starting a merge.  Doing so will help you abort a failed merge, should the need arise. +" [short_path $path]] +			unlock_index +			return 0 +		} +		} +	} + +	return 1 +} + +method _rev {} { +	if {[catch {$w_rev commit_or_die}]} { +		return {} +	} +	return [$w_rev get] +} + +method _visualize {} { +	set rev [_rev $this] +	if {$rev ne {}} { +		do_gitk [list $rev --not HEAD] +	} +} + +method _start {} { +	global HEAD current_branch remote_url +	global _last_merged_branch + +	set name [_rev $this] +	if {$name eq {}} { +		return +	} + +	set spec [$w_rev get_tracking_branch] +	set cmit [$w_rev get_commit] + +	set fh [safe_open_file [gitdir FETCH_HEAD] w] +	fconfigure $fh -translation lf +	if {$spec eq {}} { +		set remote . +		set branch $name +		set stitle $branch +	} else { +		set remote $remote_url([lindex $spec 1]) +		if {[regexp {^[^:@]*@[^:]*:/} $remote]} { +			regsub {^[^:@]*@} $remote {} remote +		} +		set branch [lindex $spec 2] +		set stitle [mc "%s of %s" $branch $remote] +	} +	regsub ^refs/heads/ $branch {} branch +	puts $fh "$cmit\t\tbranch '$branch' of $remote" +	close $fh +	set _last_merged_branch $branch + +	set cmd [list git merge --strategy=recursive FETCH_HEAD] + +	ui_status [mc "Merging %s and %s..." $current_branch $stitle] +	set cons [console::new [mc "Merge"] "merge $stitle"] +	console::exec $cons $cmd [cb _finish $cons] + +	wm protocol $w WM_DELETE_WINDOW {} +	destroy $w +} + +method _finish {cons ok} { +	console::done $cons $ok +	if {$ok} { +		set msg [mc "Merge completed successfully."] +	} else { +		set msg [mc "Merge failed.  Conflict resolution is required."] +	} +	unlock_index +	rescan [list ui_status $msg] +	delete_this +} + +constructor dialog {} { +	global current_branch +	global M1B + +	if {![_can_merge $this]} { +		delete_this +		return +	} + +	make_dialog top w +	wm title $top [mc "%s (%s): Merge" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	set _start [cb _start] + +	ttk::label $w.header \ +		-text [mc "Merge Into %s" $current_branch] \ +		-font font_uibold +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.visualize \ +		-text [mc Visualize] \ +		-command [cb _visualize] +	pack $w.buttons.visualize -side left +	ttk::button $w.buttons.merge \ +		-text [mc Merge] \ +		-command $_start +	pack $w.buttons.merge -side right +	ttk::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 + +	set w_rev [::choose_rev::new_unmerged $w.rev [mc "Revision To Merge"]] +	pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + +	bind $w <$M1B-Key-Return> $_start +	bind $w <Key-Return> $_start +	bind $w <Key-Escape> [cb _cancel] +	wm protocol $w WM_DELETE_WINDOW [cb _cancel] + +	bind $w.buttons.merge <Visibility> [cb _visible] +	tkwait window $w +} + +method _visible {} { +	grab $w +	if {[is_config_true gui.matchtrackingbranch]} { +		$w_rev pick_tracking_branch +	} +	$w_rev focus_filter +} + +method _cancel {} { +	wm protocol $w WM_DELETE_WINDOW {} +	unlock_index +	destroy $w +	delete_this +} + +} + +namespace eval merge { + +proc reset_hard {} { +	global HEAD commit_type file_states + +	if {[string match amend* $commit_type]} { +		info_popup [mc "Cannot abort while amending. + +You must finish amending this commit. +"] +		return +	} + +	if {![lock_index abort]} return + +	if {[string match *merge* $commit_type]} { +		set op_question [mc "Abort merge? + +Aborting the current merge will cause *ALL* uncommitted changes to be lost. + +Continue with aborting the current merge?"] +	} else { +		set op_question [mc "Reset changes? + +Resetting the changes will cause *ALL* uncommitted changes to be lost. + +Continue with resetting the current changes?"] +	} + +	if {[ask_popup $op_question] eq {yes}} { +		set fd [git_read [list read-tree --reset -u -v HEAD] [list 2>@1]] +		fconfigure $fd -blocking 0 -translation binary +		set status_bar_operation [$::main_status \ +			start \ +			[mc "Aborting"] \ +			[mc "files reset"]] +		fileevent $fd readable [namespace code [list \ +			_reset_wait $fd $status_bar_operation]] +	} else { +		unlock_index +	} +} + +proc _reset_wait {fd status_bar_operation} { +	global ui_comm + +	$status_bar_operation update_meter [read $fd] + +	fconfigure $fd -blocking 1 +	if {[eof $fd]} { +		set fail [catch {close $fd} err] +		unlock_index +		$status_bar_operation stop + +		$ui_comm delete 0.0 end +		$ui_comm edit modified false + +		catch {file delete [gitdir MERGE_HEAD]} +		catch {file delete [gitdir rr-cache MERGE_RR]} +		catch {file delete [gitdir MERGE_RR]} +		catch {file delete [gitdir SQUASH_MSG]} +		catch {file delete [gitdir MERGE_MSG]} +		catch {file delete [gitdir GITGUI_MSG]} + +		if {$fail} { +			warn_popup "[mc "Abort failed."]\n\n$err" +		} +		rescan {ui_status [mc "Abort completed.  Ready."]} +	} else { +		fconfigure $fd -blocking 0 +	} +} + +} diff --git a/git-gui/lib/mergetool.tcl b/git-gui/lib/mergetool.tcl new file mode 100644 index 0000000000..44be4ed3ff --- /dev/null +++ b/git-gui/lib/mergetool.tcl @@ -0,0 +1,417 @@ +# 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 {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 [git_read [list ls-files -u -z -- $path]] + +	fconfigure $merge_stages_fd -blocking 0 -translation 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 { +	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"] +		} +	} +	bc3 { +		if {$base_stage ne {}} { +			set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" "$BASE" "-mergeoutput=$MERGED"] +		} else { +			set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" "-mergeoutput=$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"] +		} +	} +	gvimdiff { +		set cmdline [list "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"] +	} +	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"] +		} +	} +	meld { +		set cmdline [list "$merge_tool_path" "$LOCAL" "$MERGED" "$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"] +		} +	} +	p4merge { +		set cmdline [list "$merge_tool_path" "$BASE" "$REMOTE" "$LOCAL" "$MERGED"] +	} +	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"] +		} +	} +	vimdiff { +		error_popup [mc "Not a GUI merge tool: '%s'" $tool] +		return +	} +	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"] +		} +	} +	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"] +		} +	} +	default { +		set tool_cmd [get_config mergetool.$tool.cmd] +		if {$tool_cmd ne {}} { +			if {([string first {[} $tool_cmd] != -1) || ([string first {]} $tool_cmd] != -1)} { +				error_popup [mc "Unable to process square brackets in \"mergetool.%s.cmd\" configuration option. + +Please remove the square brackets." $tool] +				return +			} else { +				set cmdline {} +				foreach command_part $tool_cmd { +					lappend cmdline [subst -nobackslashes -nocommands $command_part] +				} +			} +		} else { +			error_popup [mc "Unsupported merge tool '%s'. + +To use this tool, configure \"mergetool.%s.cmd\" as shown in the git-config manual page." $tool $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 [safe_open_file $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 +	set redir [list {2>@1}] + +	if {[catch { set mtool_fd [safe_open_command $cmdline $redir] } 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 +	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 mergetool.keepbackup]} { +			file rename -force -- $backup "$mtool_target.orig" +		} + +		delete_temp_files $mtool_tmpfiles + +		reshow_diff +	} +} diff --git a/git-gui/lib/meson.build b/git-gui/lib/meson.build new file mode 100644 index 0000000000..4b9efab774 --- /dev/null +++ b/git-gui/lib/meson.build @@ -0,0 +1,74 @@ +libfiles = [ +  'about.tcl', +  'blame.tcl', +  'branch_checkout.tcl', +  'branch_create.tcl', +  'branch_delete.tcl', +  'branch_rename.tcl', +  'branch.tcl', +  'browser.tcl', +  'checkout_op.tcl', +  'choose_font.tcl', +  'choose_repository.tcl', +  'choose_rev.tcl', +  'chord.tcl', +  'class.tcl', +  'commit.tcl', +  'console.tcl', +  'database.tcl', +  'date.tcl', +  'diff.tcl', +  'encoding.tcl', +  'error.tcl', +  'index.tcl', +  'line.tcl', +  'logo.tcl', +  'merge.tcl', +  'mergetool.tcl', +  'option.tcl', +  'remote_add.tcl', +  'remote_branch_delete.tcl', +  'remote.tcl', +  'search.tcl', +  'shortcut.tcl', +  'spellcheck.tcl', +  'sshkey.tcl', +  'status_bar.tcl', +  'themed.tcl', +  'tools_dlg.tcl', +  'tools.tcl', +  'transport.tcl', +  'win32.tcl', +] + +nontcl_libfiles = [ +  'git-gui.ico', +  'win32_shortcut.js', +] + +foreach file : libfiles + nontcl_libfiles +  configure_file( +    input: file, +    output: file, +    copy: true, +    install: true, +    install_dir: get_option('datadir') / 'git-gui/lib', +  ) +endforeach + +custom_target( +  output: 'tclIndex', +  command: [ +    shell, +    meson.project_source_root() / 'generate-tclindex.sh', +    meson.project_build_root(), +    meson.project_build_root() / 'GIT-GUI-BUILD-OPTIONS', +    libfiles, +  ], +  depend_files: [ +    libfiles, +    build_options, +  ], +  install: true, +  install_dir: get_option('datadir') / 'git-gui/lib', +) diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl new file mode 100644 index 0000000000..487d70691d --- /dev/null +++ b/git-gui/lib/option.tcl @@ -0,0 +1,337 @@ +# 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 system_config +	global repo_config_new global_config_new +	global ui_comm_spell + +	foreach option $font_descs { +		set name [lindex $option 0] +		set font [lindex $option 1] +		font configure $font \ +			-family $global_config_new(gui.$font^^family) \ +			-size $global_config_new(gui.$font^^size) +		font configure ${font}bold \ +			-family $global_config_new(gui.$font^^family) \ +			-size $global_config_new(gui.$font^^size) +		font configure ${font}italic \ +			-family $global_config_new(gui.$font^^family) \ +			-size $global_config_new(gui.$font^^size) +		set global_config_new(gui.$name) [font configure $font] +		unset global_config_new(gui.$font^^family) +		unset global_config_new(gui.$font^^size) +	} + +	foreach name [array names default_config] { +		set value $global_config_new($name) +		if {$value ne $global_config($name)} { +			if {$value eq $system_config($name)} { +				catch {git config --global --unset $name} +			} else { +				regsub -all "\[{}\]" $value {"} value +				git config --global $name $value +			} +			set global_config($name) $value +			if {$value eq $repo_config($name)} { +				catch {git config --unset $name} +				set repo_config($name) $value +			} +		} +	} + +	foreach name [array names default_config] { +		set value $repo_config_new($name) +		if {$value ne $repo_config($name)} { +			if {$value eq $global_config($name)} { +				catch {git config --unset $name} +			} else { +				regsub -all "\[{}\]" $value {"} value +				git config $name $value +			} +			set repo_config($name) $value +		} +	} + +	if {[info exists repo_config(gui.spellingdictionary)]} { +		set value $repo_config(gui.spellingdictionary) +		if {$value eq {none}} { +			if {[info exists ui_comm_spell]} { +				$ui_comm_spell stop +			} +		} elseif {[info exists ui_comm_spell]} { +			$ui_comm_spell lang $value +		} +	} +} + +proc do_options {} { +	global repo_config global_config font_descs +	global repo_config_new global_config_new +	global ui_comm_spell + +	array unset repo_config_new +	array unset global_config_new +	foreach name [array names repo_config] { +		set repo_config_new($name) $repo_config($name) +	} +	load_config 1 +	foreach name [array names repo_config] { +		switch -- $name { +		gui.diffcontext {continue} +		} +		set repo_config_new($name) $repo_config($name) +	} +	foreach name [array names global_config] { +		set global_config_new($name) $global_config($name) +	} + +	set w .options_editor +	Dialog $w +	wm withdraw $w +	wm transient $w [winfo parent $w] +	wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + +	ttk::frame $w.buttons +	ttk::button $w.buttons.restore -text [mc "Restore Defaults"] \ +		-default normal \ +		-command do_restore_defaults +	pack $w.buttons.restore -side left +	ttk::button $w.buttons.save -text [mc Save] \ +		-default active \ +		-command [list do_save_config $w] +	pack $w.buttons.save -side right +	ttk::button $w.buttons.cancel -text [mc "Cancel"] \ +		-default normal \ +		-command [list destroy $w] +	pack $w.buttons.cancel -side right -padx 5 +	pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + +	ttk::labelframe $w.repo -text [mc "%s Repository" [reponame]] +	ttk::labelframe $w.global -text [mc "Global (All Repositories)"] +	pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5 +	pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5 + +	set optid 0 +	foreach option { +		{t user.name {mc "User Name"}} +		{t user.email {mc "Email Address"}} + +		{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.textconv {mc "Use Textconv For Diffs and Blames"}} +		{b gui.fastcopyblame {mc "Blame Copy Only On Changed Files"}} +		{i-0..100 gui.maxrecentrepo {mc "Maximum Length of Recent Repositories List"}} +		{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"}} +		{t gui.diffopts {mc "Additional Diff Parameters"}} +		{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"}} +		{b gui.warndetachedcommit {mc "Warn before committing to a detached head"}} +		{s gui.stageuntracked {mc "Staging of untracked files"} {list "yes" "no" "ask"}} +		{b gui.displayuntracked {mc "Show untracked files"}} +		{i-1..99 gui.tabsize {mc "Tab spacing"}} +		} { +		set type [lindex $option 0] +		set name [lindex $option 1] +		set text [eval [lindex $option 2]] +		incr optid +		foreach f {repo global} { +			switch -glob -- $type { +			b { +				ttk::checkbutton $w.$f.$optid -text $text \ +					-variable ${f}_config_new($name) \ +					-onvalue true \ +					-offvalue false +				pack $w.$f.$optid -side top -anchor w +			} +			i-* { +				regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max +				ttk::frame $w.$f.$optid +				ttk::label $w.$f.$optid.l -text [mc "%s:" $text] +				pack $w.$f.$optid.l -side left -anchor w -fill x +				tspinbox $w.$f.$optid.v \ +					-textvariable ${f}_config_new($name) \ +					-from $min \ +					-to $max \ +					-increment 1 \ +					-width [expr {1 + [string length $max]}] +				bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end} +				pack $w.$f.$optid.v -side right -anchor e -padx 5 +				pack $w.$f.$optid -side top -anchor w -fill x +			} +			c - +			t { +				ttk::frame $w.$f.$optid +				ttk::label $w.$f.$optid.l -text [mc "%s:" $text] +				ttk::entry $w.$f.$optid.v \ +					-width 20 \ +					-textvariable ${f}_config_new($name) +				pack $w.$f.$optid.l -side left -anchor w +				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 +					ttk::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 +			} +			s { +				set opts [eval [lindex $option 3]] +				ttk::frame $w.$f.$optid +				ttk::label $w.$f.$optid.l -text [mc "%s:" $text] +				ttk::combobox $w.$f.$optid.v \ +					-textvariable ${f}_config_new($name) \ +					-values $opts -state readonly +				pack $w.$f.$optid.l -side left -anchor w -fill x +				pack $w.$f.$optid.v -side right -anchor e -padx 5 +				pack $w.$f.$optid -side top -anchor w -fill x +			} +			} +		} +	} + +	set all_dicts [linsert \ +		[spellcheck::available_langs] \ +		0 \ +		none] +	incr optid +	foreach f {repo global} { +		if {![info exists ${f}_config_new(gui.spellingdictionary)]} { +			if {[info exists ui_comm_spell]} { +				set value [$ui_comm_spell lang] +			} else { +				set value none +			} +			set ${f}_config_new(gui.spellingdictionary) $value +		} + +		ttk::frame $w.$f.$optid +		ttk::label $w.$f.$optid.l -text [mc "Spelling Dictionary:"] +		ttk::combobox $w.$f.$optid.v \ +			-textvariable ${f}_config_new(gui.spellingdictionary) \ +			-values $all_dicts -state readonly +		pack $w.$f.$optid.l -side left -anchor w -fill x +		pack $w.$f.$optid.v -side right -anchor e -padx 5 +		pack $w.$f.$optid -side top -anchor w -fill x +	} +	unset all_dicts + +	set all_fonts [lsort [font families]] +	foreach option $font_descs { +		set name [lindex $option 0] +		set font [lindex $option 1] +		set text [eval [lindex $option 2]] + +		set global_config_new(gui.$font^^family) \ +			[font configure $font -family] +		set global_config_new(gui.$font^^size) \ +			[font configure $font -size] + +		ttk::frame $w.global.$name +		ttk::label $w.global.$name.l -text [mc "%s:" $text] +		ttk::button $w.global.$name.b \ +			-text [mc "Change Font"] \ +			-command [list \ +				tchoosefont \ +				$w \ +				[mc "Choose %s" $text] \ +				global_config_new(gui.$font^^family) \ +				global_config_new(gui.$font^^size) \ +				] +		ttk::label $w.global.$name.f -textvariable global_config_new(gui.$font^^family) +		ttk::label $w.global.$name.s -textvariable global_config_new(gui.$font^^size) +		ttk::label $w.global.$name.pt -text [mc "pt."] +		pack $w.global.$name.l -side left -anchor w +		pack $w.global.$name.b -side right -anchor e +		pack $w.global.$name.pt -side right -anchor w +		pack $w.global.$name.s -side right -anchor w +		pack $w.global.$name.f -side right -anchor w +		pack $w.global.$name -side top -anchor w -fill x +	} + +	bind $w <Visibility> "grab $w; focus $w.buttons.save" +	bind $w <Key-Escape> "destroy $w" +	bind $w <Key-Return> [list do_save_config $w] + +	if {[is_MacOSX]} { +		set t [mc "Preferences"] +	} else { +		set t [mc "Options"] +	} +	wm title $w "[appname] ([reponame]): $t" +	wm deiconify $w +	tkwait window $w +} + +proc do_restore_defaults {} { +	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) $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) $system_config(gui.$name) +	} +	apply_config + +	foreach option $font_descs { +		set name [lindex $option 0] +		set font [lindex $option 1] +		set global_config_new(gui.$font^^family) \ +			[font configure $font -family] +		set global_config_new(gui.$font^^size) \ +			[font configure $font -size] +	} +} + +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"] +	} +	reshow_diff +	destroy $w +} diff --git a/git-gui/lib/remote.tcl b/git-gui/lib/remote.tcl new file mode 100644 index 0000000000..9b49b6e462 --- /dev/null +++ b/git-gui/lib/remote.tcl @@ -0,0 +1,331 @@ +# git-gui remote management +# Copyright (C) 2006, 2007 Shawn Pearce + +set some_heads_tracking 0;  # assume not + +proc is_tracking_branch {name} { +	global tracking_branches +	foreach spec $tracking_branches { +		set t [lindex $spec 0] +		if {$t eq $name || [string match $t $name]} { +			return 1 +		} +	} +	return 0 +} + +proc all_tracking_branches {} { +	global tracking_branches + +	set all [list] +	set pat [list] +	set cmd [list] + +	foreach spec $tracking_branches { +		set dst [lindex $spec 0] +		if {[string range $dst end-1 end] eq {/*}} { +			lappend pat $spec +			lappend cmd [string range $dst 0 end-2] +		} else { +			lappend all $spec +		} +	} + +	if {$pat ne {}} { +		set fd [git_read [concat for-each-ref --format=%(refname) $cmd]] +		while {[gets $fd n] > 0} { +			foreach spec $pat { +				set dst [string range [lindex $spec 0] 0 end-2] +				set len [string length $dst] +				if {[string equal -length $len $dst $n]} { +					set src [string range [lindex $spec 2] 0 end-2] +					set spec [list \ +						$n \ +						[lindex $spec 1] \ +						$src[string range $n $len end] \ +						] +					lappend all $spec +				} +			} +		} +		close $fd +	} + +	return [lsort -index 0 -unique $all] +} + +proc load_all_remotes {} { +	global repo_config +	global all_remotes tracking_branches some_heads_tracking +	global remote_url + +	set some_heads_tracking 0 +	set all_remotes [list] +	set trck [list] + +	set rh_str refs/heads/ +	set rh_len [string length $rh_str] +	set rm_dir [gitdir remotes] +	if {[file isdirectory $rm_dir]} { +		set all_remotes [glob \ +			-types f \ +			-tails \ +			-nocomplain \ +			-directory $rm_dir *] + +		foreach name $all_remotes { +			catch { +				set fd [safe_open_file [file join $rm_dir $name] r] +				while {[gets $fd line] >= 0} { +					if {[regexp {^URL:[ 	]*(.+)$} $line line url]} { +						set remote_url($name) $url +						continue +					} +					if {![regexp {^Pull:[ 	]*([^:]+):(.+)$} \ +						$line line src dst]} continue +					if {[string index $src 0] eq {+}} { +						set src [string range $src 1 end] +					} +					if {![string equal -length 5 refs/ $src]} { +						set src $rh_str$src +					} +					if {![string equal -length 5 refs/ $dst]} { +						set dst $rh_str$dst +					} +					if {[string equal -length $rh_len $rh_str $dst]} { +						set some_heads_tracking 1 +					} +					lappend trck [list $dst $name $src] +				} +				close $fd +			} +		} +	} + +	foreach line [array names repo_config remote.*.url] { +		if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue +		lappend all_remotes $name +		set remote_url($name) $repo_config(remote.$name.url) + +		if {[catch {set fl $repo_config(remote.$name.fetch)}]} { +			set fl {} +		} +		foreach line $fl { +			if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue +			if {[string index $src 0] eq {+}} { +				set src [string range $src 1 end] +			} +			if {![string equal -length 5 refs/ $src]} { +				set src $rh_str$src +			} +			if {![string equal -length 5 refs/ $dst]} { +				set dst $rh_str$dst +			} +			if {[string equal -length $rh_len $rh_str $dst]} { +				set some_heads_tracking 1 +			} +			lappend trck [list $dst $name $src] +		} +	} + +	set tracking_branches [lsort -index 0 -unique $trck] +	set all_remotes [lsort -unique $all_remotes] +} + +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 +	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 [safe_open_file [gitdir remotes $r] r] +			while {[gets $fd n] >= 0} { +				if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { +					set enable 1 +					break +				} +			} +			close $fd +		} +	} + +	if {$enable} { +		make_sure_remote_submenues_exist $remote_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 add_push_entry {r} { +	global repo_config +	set remote_m .mbar.remote +	set push_m $remote_m.push +	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 [safe_open_file [gitdir remotes $r] r] +			while {[gets $fd n] >= 0} { +				if {[regexp {^Push:[ \t]*([^:]+):} $n]} { +					set enable 1 +					break +				} +			} +			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] +	} +} + +proc make_sure_remote_submenues_exist {remote_m} { +	set fetch_m $remote_m.fetch +	set prune_m $remote_m.prune +	set remove_m $remote_m.remove + +	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 + +		menu $fetch_m +		$remote_m insert 0 cascade \ +			-label [mc "Fetch from"] \ +			-menu $fetch_m +	} +} + +proc update_all_remotes_menu_entry {} { +	global all_remotes + +	set have_remote 0 +	foreach r $all_remotes { +		incr have_remote +	} + +	set remote_m .mbar.remote +	set fetch_m $remote_m.fetch +	set prune_m $remote_m.prune +	if {$have_remote > 1} { +		make_sure_remote_submenues_exist $remote_m +		if {[$fetch_m type end] eq "command" \ +				&& [$fetch_m entrycget end -label] ne [mc "All"]} { + +			$fetch_m insert end separator +			$fetch_m insert end command \ +				-label [mc "All"] \ +				-command fetch_from_all + +			$prune_m insert end separator +			$prune_m insert end command \ +				-label [mc "All"] \ +				-command prune_from_all +		} +	} else { +		if {[winfo exists $fetch_m]} { +			if {[$fetch_m type end] eq "command" \ +					&& [$fetch_m entrycget end -label] eq [mc "All"]} { + +				delete_from_menu $fetch_m end +				delete_from_menu $fetch_m end + +				delete_from_menu $prune_m end +				delete_from_menu $prune_m end +			} +		} +	} +} + +proc populate_remotes_menu {} { +	global all_remotes + +	foreach r $all_remotes { +		add_fetch_entry $r +		add_push_entry $r +	} + +	update_all_remotes_menu_entry +} + +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 + +	update_all_remotes_menu_entry +} + +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] +	set all_remotes [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 } + +	update_all_remotes_menu_entry +} diff --git a/git-gui/lib/remote_add.tcl b/git-gui/lib/remote_add.tcl new file mode 100644 index 0000000000..bff1376cb3 --- /dev/null +++ b/git-gui/lib/remote_add.tcl @@ -0,0 +1,190 @@ +# 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_dialog top w +	wm withdraw $top +	wm title $top [mc "%s (%s): Add Remote" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	ttk::label $w.header -text [mc "Add New Remote"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.create -text [mc Add] \ +		-default active \ +		-command [cb _add] +	pack $w.buttons.create -side right +	ttk::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 + +	ttk::labelframe $w.desc -text [mc "Remote Details"] + +	ttk::label $w.desc.name_l -text [mc "Name:"] +	set w_name $w.desc.name_t +	ttk::entry $w_name \ +		-width 40 \ +		-textvariable @name \ +		-validate key \ +		-validatecommand [cb _validate_name %d %S] +	grid $w.desc.name_l $w_name -sticky we -padx {0 5} + +	ttk::label $w.desc.loc_l -text [mc "Location:"] +	set w_loc $w.desc.loc_t +	ttk::entry $w_loc \ +		-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 + +	ttk::labelframe $w.action -text [mc "Further Action"] + +	ttk::radiobutton $w.action.fetch \ +		-text [mc "Fetch Immediately"] \ +		-value fetch \ +		-variable @opt_action +	pack $w.action.fetch -anchor nw + +	ttk::radiobutton $w.action.push \ +		-text [mc "Initialize Remote Repository and Push"] \ +		-value push \ +		-variable @opt_action +	pack $w.action.push -anchor nw + +	ttk::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 +	wm deiconify $top +	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 new file mode 100644 index 0000000000..f0814efdd7 --- /dev/null +++ b/git-gui/lib/remote_branch_delete.tcl @@ -0,0 +1,356 @@ +# git-gui remote branch deleting support +# Copyright (C) 2007 Shawn Pearce + +class remote_branch_delete { + +field w +field head_m + +field urltype   {url} +field remote    {} +field url       {} + +field checktype  {head} +field check_head {} + +field status    {} +field idle_id   {} +field full_list {} +field head_list {} +field active_ls {} +field head_cache +field full_cache +field cached + +constructor dialog {} { +	global all_remotes M1B + +	make_dialog top w +	wm title $top [mc "%s (%s): Delete Branch Remotely" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +	} + +	ttk::label $w.header -text [mc "Delete Branch Remotely"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.delete -text [mc Delete] \ +		-default active \ +		-command [cb _delete] +	pack $w.buttons.delete -side right +	ttk::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 + +	ttk::labelframe $w.dest -text [mc "From Repository"] +	if {$all_remotes ne {}} { +		ttk::radiobutton $w.dest.remote_r \ +			-text [mc "Remote:"] \ +			-value remote \ +			-variable @urltype +		ttk::combobox $w.dest.remote_m -textvariable @remote \ +			-values $all_remotes -state readonly +		grid $w.dest.remote_r $w.dest.remote_m -sticky w +		if {[lsearch -sorted -exact $all_remotes origin] != -1} { +			set remote origin +		} else { +			set remote [lindex $all_remotes 0] +		} +		set urltype remote +		trace add variable @remote write [cb _write_remote] +	} else { +		set urltype url +	} +	ttk::radiobutton $w.dest.url_r \ +		-text [mc "Arbitrary Location:"] \ +		-value url \ +		-variable @urltype +	ttk::entry $w.dest.url_t \ +		-width 50 \ +		-textvariable @url \ +		-validate key \ +		-validatecommand { +			if {%d == 1 && [regexp {\s} %S]} {return 0} +			return 1 +		} +	trace add variable @url write [cb _write_url] +	grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} +	grid columnconfigure $w.dest 1 -weight 1 +	pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + +	ttk::labelframe $w.heads -text [mc "Branches"] +	slistbox $w.heads.l \ +		-height 10 \ +		-width 70 \ +		-listvariable @head_list \ +		-selectmode extended + +	ttk::frame $w.heads.footer +	ttk::label $w.heads.footer.status \ +		-textvariable @status \ +		-anchor w \ +		-justify left +	ttk::button $w.heads.footer.rescan \ +		-text [mc "Rescan"] \ +		-command [cb _rescan] +	pack $w.heads.footer.status -side left -fill x +	pack $w.heads.footer.rescan -side right + +	pack $w.heads.footer -side bottom -fill x +	pack $w.heads.l -side left -fill both -expand 1 +	pack $w.heads -fill both -expand 1 -pady 5 -padx 5 + +	ttk::labelframe $w.validate -text [mc "Delete Only If"] +	ttk::radiobutton $w.validate.head_r \ +		-text [mc "Merged Into:"] \ +		-value head \ +		-variable @checktype +	set head_m [tk_optionMenu $w.validate.head_m @check_head {}] +	trace add variable @head_list write [cb _write_head_list] +	trace add variable @check_head write [cb _write_check_head] +	grid $w.validate.head_r $w.validate.head_m -sticky w +	ttk::radiobutton $w.validate.always_r \ +		-text [mc "Always (Do not perform merge checks)"] \ +		-value always \ +		-variable @checktype +	grid $w.validate.always_r -columnspan 2 -sticky w +	grid columnconfigure $w.validate 1 -weight 1 +	pack $w.validate -anchor nw -fill x -pady 5 -padx 5 + +	trace add variable @urltype write [cb _write_urltype] +	_rescan $this + +	bind $w <Key-F5>     [cb _rescan] +	bind $w <$M1B-Key-r> [cb _rescan] +	bind $w <$M1B-Key-R> [cb _rescan] +	bind $w <Key-Return> [cb _delete] +	bind $w <Key-Escape> [list destroy $w] +	return $w +} + +method _delete {} { +	switch $urltype { +	remote {set uri $remote} +	url    {set uri $url} +	} + +	set cache $urltype:$uri +	set crev {} +	if {$checktype eq {head}} { +		if {$check_head eq {}} { +			tk_messageBox \ +				-icon error \ +				-type ok \ +				-title [wm title $w] \ +				-parent $w \ +				-message [mc "A branch is required for 'Merged Into'."] +			return +		} +		set crev $full_cache("$cache\nrefs/heads/$check_head") +	} + +	set not_merged [list] +	set need_fetch 0 +	set have_selection 0 +	set push_cmd [list git push] +	lappend push_cmd -v +	lappend push_cmd $uri + +	foreach i [$w.heads.l curselection] { +		set ref [lindex $full_list $i] +		if {$crev ne {}} { +			set obj $full_cache("$cache\n$ref") +			if {[catch {set m [git merge-base $obj $crev]}]} { +				set need_fetch 1 +				set m {} +			} +			if {$obj ne $m} { +				lappend not_merged [lindex $head_list $i] +				continue +			} +		} + +		lappend push_cmd :$ref +		set have_selection 1 +	} + +	if {$not_merged ne {}} { +		set msg [mc "The following branches are not completely merged into %s: + + - %s" $check_head [join $not_merged "\n - "]] + +		if {$need_fetch} { +			append msg "\n\n" [mc "One or more of the merge tests failed because you have not fetched the necessary commits.  Try fetching from %s first." $uri] +		} + +		tk_messageBox \ +			-icon info \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message $msg +		if {!$have_selection} return +	} + +	if {!$have_selection} { +		tk_messageBox \ +			-icon error \ +			-type ok \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Please select one or more branches to delete."] +		return +	} + +	if {$checktype ne {head}} { +		if {[tk_messageBox \ +			-icon warning \ +			-type yesno \ +			-title [wm title $w] \ +			-parent $w \ +			-message [mc "Recovering deleted branches is difficult.\n\nDelete the selected branches?"]] ne yes} { +			return +		} +	} + +	destroy $w + +	set cons [console::new \ +		"push $uri" \ +		[mc "Deleting branches from %s" $uri]] +	console::exec $cons $push_cmd +} + +method _rescan {{force 1}} { +	switch $urltype { +	remote {set uri $remote} +	url    {set uri $url} +	} + +	if {$force} { +		unset -nocomplain cached($urltype:$uri) +	} + +	if {$idle_id ne {}} { +		after cancel $idle_id +		set idle_id {} +	} + +	_load $this $urltype:$uri $uri +} + +method _write_remote     {args} { set urltype remote } +method _write_url        {args} { set urltype url    } +method _write_check_head {args} { set checktype head } + +method _write_head_list {args} { +	global current_branch _last_merged_branch + +	$head_m delete 0 end +	foreach abr $head_list { +		$head_m insert end radiobutton \ +			-label $abr \ +			-value $abr \ +			-variable @check_head +	} +	if {[lsearch -exact -sorted $head_list $check_head] < 0} { +		if {[lsearch -exact -sorted $head_list $current_branch] < 0} { +			set check_head {} +		} else { +			set check_head $current_branch +		} +	} +	set lmb [lsearch -exact -sorted $head_list $_last_merged_branch] +	if {$lmb >= 0} { +		$w.heads.l conf -state normal +		$w.heads.l select set $lmb +		$w.heads.l yview $lmb +		$w.heads.l conf -state disabled +	} +} + +method _write_urltype {args} { +	if {$urltype eq {url}} { +		if {$idle_id ne {}} { +			after cancel $idle_id +		} +		_load $this none: {} +		set idle_id [after 1000 [cb _rescan 0]] +	} else { +		_rescan $this 0 +	} +} + +method _load {cache uri} { +	if {$active_ls ne {}} { +		catch {close $active_ls} +	} + +	if {$uri eq {}} { +		$w.heads.l conf -state disabled +		set head_list [list] +		set full_list [list] +		set status [mc "No repository selected."] +		return +	} + +	if {[catch {set x $cached($cache)}]} { +		set status [mc "Scanning %s..." $uri] +		$w.heads.l conf -state disabled +		set head_list [list] +		set full_list [list] +		set head_cache($cache) [list] +		set full_cache($cache) [list] +		set active_ls [git_read [list ls-remote $uri]] +		fconfigure $active_ls \ +			-blocking 0 \ +			-encoding utf-8 +		fileevent $active_ls readable [cb _read $cache $active_ls] +	} else { +		set status {} +		set full_list $full_cache($cache) +		set head_list $head_cache($cache) +		$w.heads.l conf -state normal +	} +} + +method _read {cache fd} { +	global hashlength + +	if {$fd ne $active_ls} { +		catch {close $fd} +		return +	} + +	while {[gets $fd line] >= 0} { +		if {[string match {*^{}} $line]} continue +		if {[regexp [string map "@@ $hashlength" {^([0-9a-f]{@@})	(.*)$}] $line _junk obj ref]} { +			if {[regsub ^refs/heads/ $ref {} abr]} { +				lappend head_list $abr +				lappend head_cache($cache) $abr +				lappend full_list $ref +				lappend full_cache($cache) $ref +				set full_cache("$cache\n$ref") $obj +			} +		} +	} + +	if {[eof $fd]} { +		if {[catch {close $fd} err]} { +			set status $err +			set head_list [list] +			set full_list [list] +		} else { +			set status {} +			set cached($cache) 1 +			$w.heads.l conf -state normal +		} +	} +} ifdeleted { +	catch {close $fd} +} + +} diff --git a/git-gui/lib/search.tcl b/git-gui/lib/search.tcl new file mode 100644 index 0000000000..47a0d8c961 --- /dev/null +++ b/git-gui/lib/search.tcl @@ -0,0 +1,299 @@ +# incremental search panel +# based on code from gitk, Copyright (C) Paul Mackerras + +class searchbar { + +field w +field ctext + +field searchstring   {} +field regexpsearch +field default_regexpsearch +field casesensitive +field default_casesensitive +field smartcase +field searchdirn     -forwards + +field history +field history_index + +field smarktop +field smarkbot + +constructor new {i_w i_text args} { +	set w      $i_w +	set ctext  $i_text + +	set default_regexpsearch [is_config_true gui.search.regexp] +	switch -- [get_config gui.search.case] { +	no { +		set default_casesensitive 0 +		set smartcase 0 +	} +	smart { +		set default_casesensitive 0 +		set smartcase 1 +	} +	yes - +	default { +		set default_casesensitive 1 +		set smartcase 0 +	} +	} + +	set history [list] + +	ttk::frame  $w +	ttk::label  $w.l       -text [mc Find:] +	tentry  $w.ent -textvariable ${__this}::searchstring -background lightgreen +	ttk::button $w.bn      -text [mc Next] -command [cb find_next] +	ttk::button $w.bp      -text [mc Prev] -command [cb find_prev] +	ttk::checkbutton $w.re -text [mc RegExp] \ +		-variable ${__this}::regexpsearch -command [cb _incrsearch] +	ttk::checkbutton $w.cs -text [mc Case] \ +		-variable ${__this}::casesensitive -command [cb _incrsearch] +	pack   $w.l   -side left +	pack   $w.cs  -side right +	pack   $w.re  -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.ent <Return> [cb find_next] +	bind $w.ent <Shift-Return> [cb find_prev] +	bind $w.ent <Key-Up>   [cb _prev_search] +	bind $w.ent <Key-Down> [cb _next_search] +	 +	bind $w <Destroy> [list delete_this $this] +	return $this +} + +method show {} { +	if {![visible $this]} { +		grid $w +		$w.ent delete 0 end +		set regexpsearch  $default_regexpsearch +		set casesensitive $default_casesensitive +		set history_index [llength $history] +	} +	focus -force $w.ent +} + +method hide {} { +	if {[visible $this]} { +		focus $ctext +		grid remove $w +		_save_search $this +	} +} + +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 {$regexpsearch} { +		lappend cmd -regexp +	} +	if {!$casesensitive} { +		lappend cmd -nocase +	} +	if {$dir eq {}} { +		set dir $searchdirn +	} +	lappend cmd $dir -- $searchstring +	if {[catch { +		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]] +			} +		} +	} err]} { set here {} } +	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 {}} { +		if {$smartcase && [regexp {[[:upper:]]} $searchstring]} { +			set casesensitive 1 +		} +		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 +			$w.ent state !pressed +			_set_marks $this 1 +		} else { +			#$w.ent configure -background lightpink +			$w.ent state pressed +		} +	} elseif {$smartcase} { +		# clearing the field resets the smart case detection +		set casesensitive 0 +	} +} + +method _save_search {} { +	if {$searchstring eq {}} { +		return +	} +	if {[llength $history] > 0} { +		foreach {s_regexp s_case s_expr} [lindex $history end] break +	} else { +		set s_regexp $regexpsearch +		set s_case   $casesensitive +		set s_expr   "" +	} +	if {$searchstring eq $s_expr} { +		# update modes +		set history [lreplace $history end end \ +				[list $regexpsearch $casesensitive $searchstring]] +	} else { +		lappend history [list $regexpsearch $casesensitive $searchstring] +	} +	set history_index [llength $history] +} + +method _prev_search {} { +	if {$history_index > 0} { +		incr history_index -1 +		foreach {s_regexp s_case s_expr} [lindex $history $history_index] break +		$w.ent delete 0 end +		$w.ent insert 0 $s_expr +		set regexpsearch $s_regexp +		set casesensitive $s_case +	} +} + +method _next_search {} { +	if {$history_index < [llength $history]} { +		incr history_index +	} +	if {$history_index < [llength $history]} { +		foreach {s_regexp s_case s_expr} [lindex $history $history_index] break +	} else { +		set s_regexp $default_regexpsearch +		set s_case   $default_casesensitive +		set s_expr   "" +	} +	$w.ent delete 0 end +	$w.ent insert 0 $s_expr +	set regexpsearch $s_regexp +	set casesensitive $s_case +} + +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 {}} { +		_save_search $this +		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] +	} +} + +} diff --git a/git-gui/lib/shortcut.tcl b/git-gui/lib/shortcut.tcl new file mode 100644 index 0000000000..431665059e --- /dev/null +++ b/git-gui/lib/shortcut.tcl @@ -0,0 +1,154 @@ +# git-gui desktop icon creators +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_windows_shortcut {} { +	global _gitworktree + +	set desktop [safe_exec [list cygpath -mD]] +	set link_file "Git [reponame].lnk" +	set link_path [file normalize [file join $desktop $link_file]] + +	# on Windows, tk_getSaveFile dereferences .lnk files, so no simple +	# filename chooser is available. Use the default or quit. +	if {[file exists $link_path]} { +		set answer [tk_messageBox \ +			-type yesno \ +			-title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ +			-default yes \ +			-message [mc "Replace existing shortcut: %s?" $link_file]] +		if {$answer == no} { +			return +		} +	} + +	# Use git-gui.exe if found, fall back to wish + launcher +	set link_arguments {} +	set link_target [safe_exec [list cygpath -m /cmd/git-gui.exe]] +	if {![file executable $link_target]} { +		set link_target [_which git-gui] +	} +	if {![file executable $link_target]} { +		set link_target [file normalize [info nameofexecutable]] +		set link_arguments [file normalize $::argv0] +	} +	set cmdLine [list $link_target $link_arguments] +	if {[catch { +		win32_create_lnk $link_path $cmdLine \ +			[file normalize $_gitworktree] +	} err]} { +		error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"] +	} +} + +proc do_cygwin_shortcut {} { +	global argv0 _gitworktree oguilib + +	if {[catch { +		set desktop [safe_exec [list cygpath \ +			--desktop]] +		}]} { +			set desktop . +	} +	set fn [tk_getSaveFile \ +		-parent . \ +		-title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ +		-initialdir $desktop \ +		-initialfile "Git [reponame].lnk"] +	if {$fn != {}} { +		if {[file extension $fn] ne {.lnk}} { +			set fn ${fn}.lnk +		} +		if {[catch { +				set repodir [file normalize $_gitworktree] +				set shargs {-c \ +					"CHERE_INVOKING=1 \ +					source /etc/profile; \ +					git gui"} +				safe_exec [list /bin/mkshortcut.exe \ +					--arguments $shargs \ +					--desc "git-gui on $repodir" \ +					--icon $oguilib/git-gui.ico \ +					--name $fn \ +					--show min \ +					--workingdir $repodir \ +					/bin/sh.exe] +			} err]} { +			error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"] +		} +	} +} + +proc do_macosx_app {} { +	global argv0 env + +	set fn [tk_getSaveFile \ +		-parent . \ +		-title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ +		-initialdir [file join $env(HOME) Desktop] \ +		-initialfile "Git [reponame].app"] +	if {$fn != {}} { +		if {[file extension $fn] ne {.app}} { +			set fn ${fn}.app +		} +		if {[catch { +				set Contents [file join $fn Contents] +				set MacOS [file join $Contents MacOS] +				set exe [file join $MacOS git-gui] + +				file mkdir $MacOS + +				set fd [safe_open_file [file join $Contents Info.plist] w] +				puts $fd {<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> +	<key>CFBundleDevelopmentRegion</key> +	<string>English</string> +	<key>CFBundleExecutable</key> +	<string>git-gui</string> +	<key>CFBundleIdentifier</key> +	<string>org.spearce.git-gui</string> +	<key>CFBundleInfoDictionaryVersion</key> +	<string>6.0</string> +	<key>CFBundlePackageType</key> +	<string>APPL</string> +	<key>CFBundleSignature</key> +	<string>????</string> +	<key>CFBundleVersion</key> +	<string>1.0</string> +	<key>NSPrincipalClass</key> +	<string>NSApplication</string> +</dict> +</plist>} +				close $fd + +				set fd [safe_open_file $exe w] +				puts $fd "#!/bin/sh" +				foreach name [lsort [array names env]] { +					set value $env($name) +					switch -- $name { +					GIT_DIR { set value [file normalize [gitdir]] } +					} + +					switch -glob -- $name { +					SSH_* - +					GIT_* { +						puts $fd "if test \"z\$$name\" = z; then" +						puts $fd "  export $name=[sq $value]" +						puts $fd "fi &&" +					} +					} +				} +				puts $fd "export PATH=[sq [file dirname $::_git]]:\$PATH &&" +				puts $fd "cd [sq [file normalize [pwd]]] &&" +				puts $fd "exec \\" +				puts $fd " [sq [info nameofexecutable]] \\" +				puts $fd " [sq [file normalize $argv0]]" +				close $fd + +				file attributes $exe -permissions u+x,g+x,o+x +			} err]} { +			error_popup [strcat [mc "Cannot write icon:"] "\n\n$err"] +		} +	} +} diff --git a/git-gui/lib/spellcheck.tcl b/git-gui/lib/spellcheck.tcl new file mode 100644 index 0000000000..634656820d --- /dev/null +++ b/git-gui/lib/spellcheck.tcl @@ -0,0 +1,414 @@ +# git-gui spellchecking support through ispell/aspell +# Copyright (C) 2008 Shawn Pearce + +class spellcheck { + +field s_fd      {} ; # pipe to ispell/aspell +field s_version {} ; # ispell/aspell version string +field s_lang    {} ; # current language code +field s_prog aspell; # are we actually old ispell? +field s_failed   0 ; # is $s_prog bogus and not working? + +field w_text      ; # text widget we are spelling +field w_menu      ; # context menu for the widget +field s_menuidx 0 ; # last index of insertion into $w_menu + +field s_i           {} ; # timer registration for _run callbacks +field s_clear        0 ; # did we erase misspelled tags yet? +field s_seen    [list] ; # lines last seen from $w_text in _run +field s_checked [list] ; # lines already checked +field s_pending [list] ; # [$line $data] sent to ispell/aspell +field s_suggest        ; # array, list of suggestions, keyed by misspelling + +constructor init {pipe_fd ui_text ui_menu} { +	set w_text $ui_text +	set w_menu $ui_menu +	array unset s_suggest + +	bind_button3 $w_text [cb _popup_suggest %X %Y @%x,%y] +	_connect $this $pipe_fd +	return $this +} + +method _connect {pipe_fd} { +	fconfigure $pipe_fd \ +		-encoding utf-8 \ +		-translation lf + +	if {[gets $pipe_fd s_version] <= 0} { +		if {[catch {close $pipe_fd} err]} { + +			# Eh?  Is this actually ispell choking on aspell options? +			# +			if {$s_prog eq {aspell} +				&& [regexp -nocase {^Usage: } $err] +				&& ![catch { +						set pipe_fd [open [list | $s_prog -v] r] +						gets $pipe_fd s_version +						close $pipe_fd +				}] +				&& $s_version ne {}} { +				if {{@(#) } eq [string range $s_version 0 4]} { +					set s_version [string range $s_version 5 end] +				} +				set s_failed 1 +				error_popup [strcat \ +					[mc "Unsupported spell checker"] \ +					":\n\n$s_version"] +				set s_version {} +				return +			} + +			regsub -nocase {^Error: } $err {} err +			if {$s_fd eq {}} { +				error_popup [strcat [mc "Spell checking is unavailable"] ":\n\n$err"] +			} else { +				error_popup [strcat \ +					[mc "Invalid spell checking configuration"] \ +					":\n\n$err\n\n" \ +					[mc "Reverting dictionary to %s." $s_lang]] +			} +		} else { +			error_popup [mc "Spell checker silently failed on startup"] +		} +		return +	} + +	if {{@(#) } ne [string range $s_version 0 4]} { +		catch {close $pipe_fd} +		error_popup [strcat [mc "Unrecognized spell checker"] ":\n\n$s_version"] +		return +	} +	set s_version [string range [string trim $s_version] 5 end] +	regexp \ +		{International Ispell Version .* \(but really (Aspell .*?)\)$} \ +		$s_version _junk s_version +	regexp {^Aspell (\d)+\.(\d+)} $s_version _junk major minor + +	puts $pipe_fd !             ; # enable terse mode + +	# fetch the language +	if {$major > 0 || ($major == 0 && $minor >= 60)} { +		puts $pipe_fd {$$cr master} +		flush $pipe_fd +		gets $pipe_fd s_lang +		regexp {[/\\]([^/\\]+)\.[^\.]+$} $s_lang _ s_lang +	} else { +		set s_lang {} +	} + +	if {$::default_config(gui.spellingdictionary) eq {} +	 && [get_config gui.spellingdictionary] eq {}} { +		set ::default_config(gui.spellingdictionary) $s_lang +	} + +	if {$s_fd ne {}} { +		catch {close $s_fd} +	} +	set s_fd $pipe_fd + +	fconfigure $s_fd -blocking 0 +	fileevent $s_fd readable [cb _read] + +	$w_text tag conf misspelled \ +		-foreground red \ +		-underline 1 + +	array unset s_suggest +	set s_seen    [list] +	set s_checked [list] +	set s_pending [list] +	_run $this +} + +method lang {{n {}}} { +	if {$n ne {} && $s_lang ne $n && !$s_failed} { +		set spell_cmd [list |] +		lappend spell_cmd aspell +		lappend spell_cmd --master=$n +		lappend spell_cmd --mode=none +		lappend spell_cmd --encoding=UTF-8 +		lappend spell_cmd pipe +		_connect $this [open $spell_cmd r+] +	} +	return $s_lang +} + +method version {} { +	if {$s_version ne {}} { +		return "$s_version, $s_lang" +	} +	return {} +} + +method stop {} { +	while {$s_menuidx > 0} { +		$w_menu delete 0 +		incr s_menuidx -1 +	} +	$w_text tag delete misspelled + +	catch {close $s_fd} +	catch {after cancel $s_i} +	set s_fd {} +	set s_i {} +	set s_lang {} +} + +method _popup_suggest {X Y pos} { +	while {$s_menuidx > 0} { +		$w_menu delete 0 +		incr s_menuidx -1 +	} + +	set b_loc [$w_text index "$pos wordstart"] +	set e_loc [_wordend $this $b_loc] +	set orig  [$w_text get $b_loc $e_loc] +	set tags  [$w_text tag names $b_loc] + +	if {[lsearch -exact $tags misspelled] >= 0} { +		if {[info exists s_suggest($orig)]} { +			set cnt 0 +			foreach s $s_suggest($orig) { +				if {$cnt < 5} { +					$w_menu insert $s_menuidx command \ +						-label $s \ +						-command [cb _replace $b_loc $e_loc $s] +					incr s_menuidx +					incr cnt +				} else { +					break +				} +			} +		} else { +			$w_menu insert $s_menuidx command \ +				-label [mc "No Suggestions"] \ +				-state disabled +			incr s_menuidx +		} +		$w_menu insert $s_menuidx separator +		incr s_menuidx +	} + +	$w_text mark set saved-insert insert +	tk_popup $w_menu $X $Y +} + +method _replace {b_loc e_loc word} { +	$w_text configure -autoseparators 0 +	$w_text edit separator + +	$w_text delete $b_loc $e_loc +	$w_text insert $b_loc $word + +	$w_text edit separator +	$w_text configure -autoseparators 1 +	$w_text mark set insert saved-insert +} + +method _restart_timer {} { +	set s_i [after 300 [cb _run]] +} + +proc _match_length {max_line arr_name} { +	upvar $arr_name a + +	if {[llength $a] > $max_line} { +		set a [lrange $a 0 $max_line] +	} +	while {[llength $a] <= $max_line} { +		lappend a {} +	} +} + +method _wordend {pos} { +	set pos  [$w_text index "$pos wordend"] +	set tags [$w_text tag names $pos] +	while {[lsearch -exact $tags misspelled] >= 0} { +		set pos  [$w_text index "$pos +1c"] +		set tags [$w_text tag names $pos] +	} +	return $pos +} + +method _run {} { +	set cur_pos  [$w_text index {insert -1c}] +	set cur_line [lindex [split $cur_pos .] 0] +	set max_line [lindex [split [$w_text index end] .] 0] +	_match_length $max_line s_seen +	_match_length $max_line s_checked + +	# Nothing in the message buffer?  Nothing to spellcheck. +	# +	if {$cur_line == 1 +	 && $max_line == 2 +	 && [$w_text get 1.0 end] eq "\n"} { +		array unset s_suggest +		_restart_timer $this +		return +	} + +	set active 0 +	for {set n 1} {$n <= $max_line} {incr n} { +		set s [$w_text get "$n.0" "$n.end"] + +		# Don't spellcheck the current line unless we are at +		# a word boundary.  The user might be typing on it. +		# +		if {$n == $cur_line +		 && ![regexp {^\W$} [$w_text get $cur_pos insert]]} { + +			# If the current word is misspelled remove the tag +			# but force a spellcheck later. +			# +			set tags [$w_text tag names $cur_pos] +			if {[lsearch -exact $tags misspelled] >= 0} { +				$w_text tag remove misspelled \ +					"$cur_pos wordstart" \ +					[_wordend $this $cur_pos] +				lset s_seen    $n $s +				lset s_checked $n {} +			} + +			continue +		} + +		if {[lindex $s_seen    $n] eq $s +		 && [lindex $s_checked $n] ne $s} { +			# Don't send empty lines to Aspell it doesn't check them. +			# +			if {$s eq {}} { +				lset s_checked $n $s +				continue +			} + +			# Don't send typical s-b-o lines as the emails are +			# almost always misspelled according to Aspell. +			# +			if {[regexp -nocase {^[a-z-]+-by:.*<.*@.*>$} $s]} { +				$w_text tag remove misspelled "$n.0" "$n.end" +				lset s_checked $n $s +				continue +			} + +			puts $s_fd ^$s +			lappend s_pending [list $n $s] +			set active 1 +		} else { +			# Delay until another idle loop to make sure we don't +			# spellcheck lines the user is actively changing. +			# +			lset s_seen $n $s +		} +	} + +	if {$active} { +		set s_clear 1 +		flush $s_fd +	} else { +		_restart_timer $this +	} +} + +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" +			set s_clear 0 +		} + +		if {$line eq {}} { +			lset s_checked $lineno [lindex $s_pending 0 1] +			set s_pending [lrange $s_pending 1 end] +			set s_clear 1 +			continue +		} + +		set sugg [list] +		switch -- [string range $line 0 1] { +		{& } { +			set line [split [string range $line 2 end] :] +			set info [split [lindex $line 0] { }] +			set orig [lindex $info 0] +			set offs [lindex $info 2] +			foreach s [split [lindex $line 1] ,] { +				lappend sugg [string range $s 1 end] +			} +		} +		{# } { +			set info [split [string range $line 2 end] { }] +			set orig [lindex $info 0] +			set offs [lindex $info 1] +		} +		default { +			puts stderr "<spell> $line" +			continue +		} +		} + +		incr offs -1 +		set b_loc "$lineno.$offs" +		set e_loc [$w_text index "$lineno.$offs wordend"] +		set curr [$w_text get $b_loc $e_loc] + +		# At least for English curr = "bob", orig = "bob's" +		# so Tk didn't include the 's but Aspell did.  We +		# try to round out the word. +		# +		while {$curr ne $orig +		 && [string equal -length [string length $curr] $curr $orig]} { +			set n_loc  [$w_text index "$e_loc +1c"] +			set n_curr [$w_text get $b_loc $n_loc] +			if {$n_curr eq $curr} { +				break +			} +			set curr  $n_curr +			set e_loc $n_loc +		} + +		if {$curr eq $orig} { +			$w_text tag add misspelled $b_loc $e_loc +			if {[llength $sugg] > 0} { +				set s_suggest($orig) $sugg +			} else { +				unset -nocomplain s_suggest($orig) +			} +		} else { +			unset -nocomplain s_suggest($orig) +		} +	} + +	fconfigure $s_fd -block 1 +	if {[eof $s_fd]} { +		if {![catch {close $s_fd} err]} { +			set err [mc "Unexpected EOF from spell checker"] +		} +		catch {after cancel $s_i} +		$w_text tag remove misspelled 1.0 end +		error_popup [strcat [mc "Spell Checker Failed"] "\n\n" $err] +		return +	} +	fconfigure $s_fd -block 0 + +	if {[llength $s_pending] == 0} { +		_restart_timer $this +	} +} + +proc available_langs {} { +	set langs [list] +	catch { +		set fd [open [list | aspell dump dicts] r] +		while {[gets $fd line] >= 0} { +			if {$line eq {}} continue +			lappend langs $line +		} +		close $fd +	} +	return $langs +} + +} diff --git a/git-gui/lib/sshkey.tcl b/git-gui/lib/sshkey.tcl new file mode 100644 index 0000000000..7a6526d3db --- /dev/null +++ b/git-gui/lib/sshkey.tcl @@ -0,0 +1,129 @@ +# git-gui about git-gui dialog +# Copyright (C) 2006, 2007 Shawn Pearce + +proc find_ssh_key {} { +	foreach name { +		~/.ssh/id_dsa.pub ~/.ssh/id_ecdsa.pub ~/.ssh/id_ed25519.pub +		~/.ssh/id_rsa.pub ~/.ssh/identity.pub +	} { +		if {[file exists $name]} { +			set fh    [safe_open_file $name r] +			set cont  [read $fh] +			close $fh +			return [list $name $cont] +		} +	} + +	return {} +} + +proc do_ssh_key {} { +	global sshkey_title sshkey_fd + +	set w .sshkey_dialog +	if {[winfo exists $w]} { +		raise $w +		return +	} + +	Dialog $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 +	} + +	ttk::frame $w.header +	ttk::label $w.header.lbl -textvariable sshkey_title -anchor w +	ttk::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 +	set clr [ttk::style lookup . -selectbackground] +	$w.contents configure -inactiveselectbackground $clr + +	ttk::frame $w.buttons +	ttk::button $w.buttons.close -text [mc Close] \ +		-default active -command [list destroy $w] +	pack $w.buttons.close -side right +	ttk::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 [shellpath] -c \ +		{echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}] + +	if {[catch { set sshkey_fd [safe_open_command $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 succeeded, 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/status_bar.tcl b/git-gui/lib/status_bar.tcl new file mode 100644 index 0000000000..f5c0204a2d --- /dev/null +++ b/git-gui/lib/status_bar.tcl @@ -0,0 +1,307 @@ +# git-gui status bar mega-widget +# Copyright (C) 2007 Shawn Pearce + +# The status_bar class manages the entire status bar. It is possible for +# multiple overlapping asynchronous operations to want to display status +# simultaneously. Each one receives a status_bar_operation when it calls the +# start method, and the status bar combines all active operations into the +# line of text it displays. Most of the time, there will be at most one +# ongoing operation. +# +# Note that the entire status bar can be either in single-line or two-line +# mode, depending on the constructor. Multiple active operations are only +# supported for single-line status bars. + +class status_bar { + +field allow_multiple ; # configured at construction + +field w         ; # our own window path +field w_l       ; # text widget we draw messages into +field w_c       ; # canvas we draw a progress bar into +field c_pack    ; # script to pack the canvas with + +field baseline_text   ; # text to show if there are no operations +field status_bar_text ; # combined text for all operations + +field operations ; # list of current ongoing operations + +# The status bar can display a progress bar, updated when consumers call the +# update method on their status_bar_operation. When there are multiple +# operations, the status bar shows the combined status of all operations. +# +# When an overlapping operation completes, the progress bar is going to +# abruptly have one fewer operation in the calculation, causing a discontinuity. +# Therefore, whenever an operation completes, if it is not the last operation, +# this counter is increased, and the progress bar is calculated as though there +# were still another operation at 100%. When the last operation completes, this +# is reset to 0. +field completed_operation_count + +constructor new {path} { +	set w $path +	set w_l $w.l +	set w_c $w.c + +	# Standard single-line status bar: Permit overlapping operations +	set allow_multiple 1 + +	set baseline_text "" +	set operations [list] +	set completed_operation_count 0 + +	ttk::frame $w +	ttk::label $w_l \ +		-textvariable @status_bar_text \ +		-anchor w \ +		-justify left +	pack $w_l -side left +	set c_pack [cb _oneline_pack] + +	bind $w <Destroy> [cb _delete %W] +	return $this +} + +method _oneline_pack {} { +	$w_c conf -width 100 +	pack $w_c -side right +} + +constructor two_line {path} { +	set w $path +	set w_l $w.l +	set w_c $w.c + +	# Two-line status bar: Only one ongoing operation permitted. +	set allow_multiple 0 + +	set baseline_text "" +	set operations [list] +	set completed_operation_count 0 + +	ttk::frame $w +	ttk::label $w_l \ +		-textvariable @status_bar_text \ +		-anchor w \ +		-justify left +	pack $w_l -anchor w -fill x +	set c_pack [list pack $w_c -fill x] + +	bind $w <Destroy> [cb _delete %W] +	return $this +} + +method ensure_canvas {} { +	if {[winfo exists $w_c]} { +		$w_c coords bar 0 0 0 20 +	} else { +		canvas $w_c \ +			-height [expr {int([winfo reqheight $w_l] * 0.6)}] \ +			-borderwidth 1 \ +			-relief groove \ +			-highlightt 0 +		$w_c create rectangle 0 0 0 20 -tags bar -fill navy +		eval $c_pack +	} +} + +method show {msg} { +	$this ensure_canvas +	set baseline_text $msg +	$this refresh +} + +method start {msg {uds {}}} { +	set baseline_text "" + +	if {!$allow_multiple && [llength $operations]} { +		return [lindex $operations 0] +	} + +	$this ensure_canvas + +	set operation [status_bar_operation::new $this $msg $uds] + +	lappend operations $operation + +	$this refresh + +	return $operation +} + +method refresh {} { +	set new_text "" + +	set total [expr $completed_operation_count * 100] +	set have $total + +	foreach operation $operations { +		if {$new_text != ""} { +			append new_text " / " +		} + +		append new_text [$operation get_status] + +		set total [expr $total + 100] +		set have [expr $have + [$operation get_progress]] +	} + +	if {$new_text == ""} { +		set new_text $baseline_text +	} + +	set status_bar_text $new_text + +	if {[winfo exists $w_c]} { +		set pixel_width 0 +		if {$have > 0} { +			set pixel_width [expr {[winfo width $w_c] * $have / $total}] +		} + +		$w_c coords bar 0 0 $pixel_width 20 +	} +} + +method stop {operation stop_msg} { +	set idx [lsearch $operations $operation] + +	if {$idx >= 0} { +		set operations [lreplace $operations $idx $idx] +		set completed_operation_count [expr \ +			$completed_operation_count + 1] + +		if {[llength $operations] == 0} { +			set completed_operation_count 0 + +			destroy $w_c +			if {$stop_msg ne {}} { +				set baseline_text $stop_msg +			} +		} + +		$this refresh +	} +} + +method stop_all {{stop_msg {}}} { +	# This makes the operation's call to stop a no-op. +	set operations_copy $operations +	set operations [list] + +	foreach operation $operations_copy { +		$operation stop +	} + +	if {$stop_msg ne {}} { +		set baseline_text $stop_msg +	} + +	$this refresh +} + +method _delete {current} { +	if {$current eq $w} { +		delete_this +	} +} + +} + +# The status_bar_operation class tracks a single consumer's ongoing status bar +# activity, with the context that there are a few situations where multiple +# overlapping asynchronous operations might want to display status information +# simultaneously. Instances of status_bar_operation are created by calling +# start on the status_bar, and when the caller is done with its stauts bar +# operation, it calls stop on the operation. + +class status_bar_operation { + +field status_bar; # reference back to the status_bar that owns this object + +field is_active; + +field status   {}; # single line of text we show +field progress {}; # current progress (0 to 100) +field prefix   {}; # text we format into status +field units    {}; # unit of progress +field meter    {}; # current core git progress meter (if active) + +constructor new {owner msg uds} { +	set status_bar $owner + +	set status $msg +	set progress 0 +	set prefix $msg +	set units  $uds +	set meter  {} + +	set is_active 1 + +	return $this +} + +method get_is_active {} { return $is_active } +method get_status {} { return $status } +method get_progress {} { return $progress } + +method update {have total} { +	if {!$is_active} { return } + +	set progress 0 + +	if {$total > 0} { +		set progress [expr {100 * $have / $total}] +	} + +	set prec [string length [format %i $total]] + +	set status [mc "%s ... %*i of %*i %s (%3i%%)" \ +		$prefix \ +		$prec $have \ +		$prec $total \ +		$units $progress] + +	$status_bar refresh +} + +method update_meter {buf} { +	if {!$is_active} { return } + +	append meter $buf +	set r [string last "\r" $meter] +	if {$r == -1} { +		return +	} + +	set prior [string range $meter 0 $r] +	set meter [string range $meter [expr {$r + 1}] end] +	set p "\\((\\d+)/(\\d+)\\)" +	if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} { +		update $this $a $b +	} elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} { +		update $this $a $b +	} +} + +method stop {{stop_msg {}}} { +	if {$is_active} { +		set is_active 0 +		$status_bar stop $this $stop_msg +	} +} + +method restart {msg} { +	if {!$is_active} { return } + +	set status $msg +	set prefix $msg +	set meter {} +	$status_bar refresh +} + +method _delete {} { +	stop +	delete_this +} + +} diff --git a/git-gui/lib/themed.tcl b/git-gui/lib/themed.tcl new file mode 100644 index 0000000000..c18e201d85 --- /dev/null +++ b/git-gui/lib/themed.tcl @@ -0,0 +1,352 @@ +# Functions for supporting the use of themed Tk widgets in git-gui. +# Copyright (C) 2009 Pat Thoyts <patthoyts@users.sourceforge.net> + + +namespace eval color { +	# Variable colors +	# Preffered way to set widget colors is using add_option. +	# In some cases, like with tags in_diff/in_sel, we use these colors. +	variable select_bg				lightgray +	variable select_fg				black +	variable inactive_select_bg		lightgray +	variable inactive_select_fg		black + +	proc sync_with_theme {} { +		set base_bg				[ttk::style lookup . -background] +		set base_fg				[ttk::style lookup . -foreground] +		set text_bg				[ttk::style lookup Treeview -background] +		set text_fg				[ttk::style lookup Treeview -foreground] +		set select_bg			[ttk::style lookup Default -selectbackground] +		set select_fg			[ttk::style lookup Default -selectforeground] +		set inactive_select_bg	[convert_rgb_to_gray $select_bg] +		set inactive_select_fg	$select_fg + +		set ::color::select_bg $select_bg +		set ::color::select_fg $select_fg +		set ::color::inactive_select_bg $inactive_select_bg +		set ::color::inactive_select_fg $inactive_select_fg + +		proc add_option {key val} { +			option add $key $val widgetDefault +		} +		# Add options for plain Tk widgets +		# Using `option add` instead of tk_setPalette to avoid unintended +		# consequences. +		if {![is_MacOSX]} { +			add_option *Menu.Background $base_bg +			add_option *Menu.Foreground $base_fg +			add_option *Menu.activeBackground $select_bg +			add_option *Menu.activeForeground $select_fg +		} +		add_option *Text.Background $text_bg +		add_option *Text.Foreground $text_fg +		add_option *Text.selectBackground $select_bg +		add_option *Text.selectForeground $select_fg +		add_option *Text.inactiveSelectBackground $inactive_select_bg +		add_option *Text.inactiveSelectForeground $inactive_select_fg +	} +} + +proc convert_rgb_to_gray {rgb} { +	# Simply take the average of red, green and blue. This wouldn't be good +	# enough for, say, converting a photo to grayscale, but for this simple +	# purpose of approximating the brightness of a color it's good enough. +	lassign [winfo rgb . $rgb] r g b +	set gray [expr {($r / 256 + $g / 256 + $b / 256) / 3}] +	return [format "#%2.2X%2.2X%2.2X" $gray $gray $gray] +} + +proc ttk_get_current_theme {} { +	# Handle either current Tk or older versions of 8.5 +	if {[catch {set theme [ttk::style theme use]}]} { +		set theme  $::ttk::currentTheme +	} +	return $theme +} + +proc InitTheme {} { +	# Create a color label style (bg can be overridden by widget option) +	ttk::style layout Color.TLabel { +		Color.Label.border -sticky news -children { +			Color.label.fill -sticky news -children { +				Color.Label.padding -sticky news -children { +					Color.Label.label -sticky news}}}} +	eval [linsert [ttk::style configure TLabel] 0 \ +			  ttk::style configure Color.TLabel] +	ttk::style configure Color.TLabel \ +		-borderwidth 0 -relief flat -padding 2 +	ttk::style map Color.TLabel -background {{} gold} +	# We also need a padded label. +	ttk::style configure Padded.TLabel \ +		-padding {5 5} -borderwidth 1 -relief solid +	# We need a gold frame. +	ttk::style layout Gold.TFrame { +		Gold.Frame.border -sticky nswe -children { +			Gold.Frame.fill -sticky nswe}} +	ttk::style configure Gold.TFrame -background gold -relief flat +	# listboxes should have a theme border so embed in ttk::frame +	ttk::style layout SListbox.TFrame { +		SListbox.Frame.Entry.field -sticky news -border true -children { +			SListbox.Frame.padding -sticky news +		} +	} + +	set theme [ttk_get_current_theme] + +	if {[lsearch -exact {default alt classic clam} $theme] != -1} { +		# Simple override of standard ttk::entry to change the field +		# packground according to a state flag. We should use 'user1' +		# but not all versions of 8.5 support that so make use of 'pressed' +		# which is not normally in use for entry widgets. +		ttk::style layout Edged.Entry [ttk::style layout TEntry] +		ttk::style map Edged.Entry {*}[ttk::style map TEntry] +		ttk::style configure Edged.Entry {*}[ttk::style configure TEntry] \ +			-fieldbackground lightgreen +		ttk::style map Edged.Entry -fieldbackground { +			{pressed !disabled} lightpink +		} +	} else { +		# For fancier themes, in particular the Windows ones, the field +		# element may not support changing the background color. So instead +		# override the fill using the default fill element. If we overrode +		# the vista theme field element we would loose the themed border +		# of the widget. +		catch { +			ttk::style element create color.fill from default +		} + +		ttk::style layout Edged.Entry { +			Edged.Entry.field -sticky nswe -border 0 -children { +				Edged.Entry.border -sticky nswe -border 1 -children { +					Edged.Entry.padding -sticky nswe -children { +						Edged.Entry.color.fill -sticky nswe -children { +							Edged.Entry.textarea -sticky nswe +						} +					} +				} +			} +		} + +		ttk::style configure Edged.Entry {*}[ttk::style configure TEntry] \ +			-background lightgreen -padding 0 -borderwidth 0 +		ttk::style map Edged.Entry {*}[ttk::style map TEntry] \ +			-background {{pressed !disabled} lightpink} +	} + +	if {[lsearch [bind . <<ThemeChanged>>] InitTheme] == -1} { +		bind . <<ThemeChanged>> +[namespace code [list InitTheme]] +	} +} + +# Define a style used for the surround of text widgets. +proc InitEntryFrame {} { +	ttk::style theme settings default { +		ttk::style layout EntryFrame { +			EntryFrame.field -sticky nswe -border 0 -children { +				EntryFrame.fill -sticky nswe -children { +					EntryFrame.padding -sticky nswe +				} +			} +		} +		ttk::style configure EntryFrame -padding 1 -relief sunken +		ttk::style map EntryFrame -background {} +	} +	ttk::style theme settings classic { +		ttk::style configure EntryFrame -padding 2 -relief sunken +		ttk::style map EntryFrame -background {} +	} +	ttk::style theme settings alt { +		ttk::style configure EntryFrame -padding 2 +		ttk::style map EntryFrame -background {} +	} +	ttk::style theme settings clam { +		ttk::style configure EntryFrame -padding 2 +		ttk::style map EntryFrame -background {} +	} + +	# Ignore errors for missing native themes +	catch { +		ttk::style theme settings winnative { +			ttk::style configure EntryFrame -padding 2 +		} +		ttk::style theme settings xpnative { +			ttk::style configure EntryFrame -padding 1 +			ttk::style element create EntryFrame.field vsapi \ +				EDIT 1 {disabled 4 focus 3 active 2 {} 1} -padding 1 +		} +		ttk::style theme settings vista { +			ttk::style configure EntryFrame -padding 2 +			ttk::style element create EntryFrame.field vsapi \ +				EDIT 6 {disabled 4 focus 3 active 2 {} 1} -padding 2 +		} +	} + +	bind EntryFrame <Enter> {%W instate !disabled {%W state active}} +	bind EntryFrame <Leave> {%W state !active} +	bind EntryFrame <<ThemeChanged>> { +		set pad [ttk::style lookup EntryFrame -padding] +		%W configure -padding [expr {$pad eq {} ? 1 : $pad}] +	} +} + +proc gold_frame {w args} { +	if {![is_MacOSX]} { +		eval [linsert $args 0 ttk::frame $w -style Gold.TFrame] +	} else { +		eval [linsert $args 0 frame $w -background gold] +	} +} + +proc tlabel {w args} { +	if {![is_MacOSX]} { +		set cmd [list ttk::label $w -style Color.TLabel] +		foreach {k v} $args { +			switch -glob -- $k { +				-activebackground {} +				default { lappend cmd $k $v } +			} +		} +		eval $cmd +	} else { +		eval [linsert $args 0 label $w] +	} +} + +# The padded label gets used in the about class. +proc paddedlabel {w args} { +	eval [linsert $args 0 ttk::label $w -style Padded.TLabel] +} + +# Create a toplevel for use as a dialog. +# If available, sets the EWMH dialog hint and if ttk is enabled +# place a themed frame over the surface. +proc Dialog {w args} { +	eval [linsert $args 0 toplevel $w -class Dialog] +	catch {wm attributes $w -type dialog} +	pave_toplevel $w +	return $w +} + +# Tk toplevels are not themed - so pave it over with a themed frame to get +# the base color correct per theme. +proc pave_toplevel {w} { +	if {![winfo exists $w.!paving]} { +		set paving [ttk::frame $w.!paving] +		place $paving -x 0 -y 0 -relwidth 1 -relheight 1 +		lower $paving +	} +} + +# Create a scrolled listbox with appropriate border for the current theme. +# On many themes the border for a scrolled listbox needs to go around the +# listbox and the scrollbar. +proc slistbox {w args} { +	set f [ttk::frame $w -style SListbox.TFrame -padding 2] +    if {[catch { +		eval [linsert $args 0 listbox $f.list -relief flat \ +				  -highlightthickness 0 -borderwidth 0] +        ttk::scrollbar $f.vs -command [list $f.list yview] +        $f.list configure -yscrollcommand [list $f.vs set] +        grid $f.list $f.vs -sticky news +        grid rowconfigure $f 0 -weight 1 +        grid columnconfigure $f 0 -weight 1 +		bind $f.list <<ListboxSelect>> \ +			[list event generate $w <<ListboxSelect>>] +        interp hide {} $w +        interp alias {} $w {} $f.list +    } err]} { +        destroy $f +        return -code error $err +    } +    return $w +} + +# fetch the background color from a widget. +proc get_bg_color {w} { +	set bg [ttk::style lookup [winfo class $w] -background] +	return $bg +} + +# ttk::spinbox +proc tspinbox {w args} { +	eval [linsert $args 0 ttk::spinbox $w] +} + +# Create a text widget with any theme specific properties. +proc ttext {w args} { +	switch -- [ttk_get_current_theme] { +		"vista" - "xpnative" { +			lappend args -highlightthickness 0 -borderwidth 0 +		} +	} +	set w [eval [linsert $args 0 text $w]] +	if {[winfo class [winfo parent $w]] eq "EntryFrame"} { +		bind $w <FocusIn> {[winfo parent %W] state focus} +		bind $w <FocusOut> {[winfo parent %W] state !focus} +	} +	return $w +} + +# themed frame suitable for surrounding a text field. +proc textframe {w args} { +	if {[catch {ttk::style layout EntryFrame}]} { +		InitEntryFrame +	} +	eval [linsert $args 0 ttk::frame $w -class EntryFrame -style EntryFrame] +	return $w +} + +proc tentry {w args} { +	InitTheme +	ttk::entry $w -style Edged.Entry + +	rename $w _$w +	interp alias {} $w {} tentry_widgetproc $w +	eval [linsert $args 0 tentry_widgetproc $w configure] +	return $w +} +proc tentry_widgetproc {w cmd args} { +	switch -- $cmd { +		state { +			return [uplevel 1 [list _$w $cmd] $args] +		} +		configure { +			if {[set n [lsearch -exact $args -background]] != -1} { +				set args [lreplace $args $n [incr n]] +				if {[llength $args] == 0} {return} +			} +			return [uplevel 1 [list _$w $cmd] $args] +		} +		default { return [uplevel 1 [list _$w $cmd] $args] } +	} +} + +# Tk 8.6 provides a standard font selection dialog. This uses the native +# dialogs on Windows and MacOSX or a standard Tk dialog on X11. +proc tchoosefont {w title familyvar sizevar} { +	if {[package vsatisfies [package provide Tk] 8.6]} { +		upvar #0 $familyvar family +		upvar #0 $sizevar size +		tk fontchooser configure -parent $w -title $title \ +			-font [list $family $size] \ +			-command [list on_choosefont $familyvar $sizevar] +		tk fontchooser show +	} else { +		choose_font::pick $w $title $familyvar $sizevar +	} +} + +# Called when the Tk 8.6 fontchooser selects a font. +proc on_choosefont {familyvar sizevar font} { +	upvar #0 $familyvar family +	upvar #0 $sizevar size +	set font [font actual $font] +	set family [dict get $font -family] +	set size [dict get $font -size] +} + +# Local variables: +# mode: tcl +# indent-tabs-mode: t +# tab-width: 4 +# End: diff --git a/git-gui/lib/tools.tcl b/git-gui/lib/tools.tcl new file mode 100644 index 0000000000..48fddfd814 --- /dev/null +++ b/git-gui/lib/tools.tcl @@ -0,0 +1,167 @@ +# 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 +	global selected_paths + +	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 {[is_config_true "guitool.$fullname.needsfile"]} { +			if {[ask_popup [mc "Are you sure you want to run %1\$s on file \"%2\$s\"?" $fullname $current_diff_path]] ne {yes}} { +				return +			} +		} else { +			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 +	set env(FILENAMES) [join [array names selected_paths] \n] +	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 [shellpath] -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 [shellpath] -c $cmdline] \ +				 [list tools_complete $fullname $w] +	} + +	unset env(GIT_GUITOOL) +	unset env(FILENAME) +	unset env(FILENAMES) +	unset env(CUR_BRANCH) +	catch { unset env(ARGS) } +	catch { unset env(REVISION) } +} + +proc tools_run_silent {cmd after} { +	set fd [safe_open_command $cmd [list 2>@1]] + +	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 successfully: %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..73236215b5 --- /dev/null +++ b/git-gui/lib/tools_dlg.tcl @@ -0,0 +1,414 @@ +# 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_dialog top w +	wm title $top [mc "%s (%s): Add Tool" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +		wm transient $top . +	} + +	ttk::label $w.header -text [mc "Add New Tool Command"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::checkbutton $w.buttons.global \ +		-text [mc "Add globally"] \ +		-variable @add_global +	pack $w.buttons.global -side left -padx 5 +	ttk::button $w.buttons.create -text [mc Add] \ +		-default active \ +		-command [cb _add] +	pack $w.buttons.create -side right +	ttk::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 + +	ttk::labelframe $w.desc -text [mc "Tool Details"] + +	ttk::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} +	ttk::label $w.desc.name_l -text [mc "Name:"] +	set w_name $w.desc.name_t +	ttk::entry $w_name \ +		-width 40 \ +		-textvariable @name \ +		-validate key \ +		-validatecommand [cb _validate_name %d %S] +	grid $w.desc.name_l $w_name -sticky we -padx {0 5} + +	ttk::label $w.desc.cmd_l -text [mc "Command:"] +	set w_cmd $w.desc.cmd_t +	ttk::entry $w_cmd \ +		-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 + +	ttk::checkbutton $w.confirm \ +		-text [mc "Show a dialog before running"] \ +		-variable @confirm -command [cb _check_enable_dlg] + +	ttk::labelframe $w.dlg -labelwidget $w.confirm + +	ttk::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 + +	ttk::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 + +	ttk::checkbutton $w.noconsole \ +		-text [mc "Don't show the command output window"] \ +		-variable @no_console +	pack $w.noconsole -anchor w -padx 5 + +	ttk::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_dialog top w +	wm title $top [mc "%s (%s): Remove Tool" [appname] [reponame]] +	if {$top ne {.}} { +		wm geometry $top "+[winfo rootx .]+[winfo rooty .]" +		wm transient $top . +	} + +	ttk::label $w.header -text [mc "Remove Tool Commands"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.create -text [mc Remove] \ +		-default active \ +		-command [cb _remove] +	pack $w.buttons.create -side right +	ttk::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 + +	ttk::frame $w.list +	set w_names $w.list.l +	slistbox $w_names \ +		-height 10 \ +		-width 30 \ +		-selectmode extended \ +		-exportselection false +	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} { +		ttk::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_dialog top w -autodelete 0 +	wm title $top "[mc "%s (%s):" [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] +	} + +	ttk::label $w.header -text $prompt -font font_uibold -anchor center +	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"] +		} + +		ttk::labelframe $w.arg -text $argprompt + +		set w_args $w.arg.txt +		ttk::entry $w_args \ +			-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 +	} + +	ttk::frame $w.buttons +	if {$is_ask_revs} { +		ttk::button $w.buttons.visualize \ +			-text [mc Visualize] \ +			-command [cb _visualize] +		pack $w.buttons.visualize -side left +	} +	ttk::button $w.buttons.ok \ +		-text [mc OK] \ +		-command [cb _start] +	pack $w.buttons.ok -side right +	ttk::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 new file mode 100644 index 0000000000..020d09e112 --- /dev/null +++ b/git-gui/lib/transport.tcl @@ -0,0 +1,228 @@ +# git-gui transport (fetch/push) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc fetch_from {remote} { +	set w [console::new \ +		[mc "fetch %s" $remote] \ +		[mc "Fetching new changes from %s" $remote]] +	set cmds [list] +	lappend cmds [list exec git fetch $remote] +	if {[is_config_true gui.pruneduringfetch]} { +		lappend cmds [list exec git remote prune $remote] +	} +	console::chain $w $cmds +} + +proc prune_from {remote} { +	set w [console::new \ +		[mc "remote prune %s" $remote] \ +		[mc "Pruning tracking branches deleted from %s" $remote]] +	console::exec $w [list git remote prune $remote] +} + +proc fetch_from_all {} { +	set w [console::new \ +		[mc "fetch all remotes"] \ +		[mc "Fetching new changes from all remotes"]] + +	set cmd [list git fetch --all] +	if {[is_config_true gui.pruneduringfetch]} { +		lappend cmd --prune +	} + +	console::exec $w $cmd +} + +proc prune_from_all {} { +	global all_remotes + +	set w [console::new \ +		[mc "remote prune all remotes"] \ +		[mc "Pruning tracking branches deleted from all remotes"]] + +	set cmd [list git remote prune] + +	foreach r $all_remotes { +		lappend cmd $r +	} + +	console::exec $w $cmd +} + +proc push_to {remote} { +	set w [console::new \ +		[mc "push %s" $remote] \ +		[mc "Pushing changes to %s" $remote]] +	set cmd [list git push] +	lappend cmd -v +	lappend cmd $remote +	console::exec $w $cmd +} + +proc start_push_anywhere_action {w} { +	global push_urltype push_remote push_url push_thin push_tags +	global push_force +	global repo_config + +	set is_mirror 0 +	set r_url {} +	switch -- $push_urltype { +	remote { +		set r_url $push_remote +		catch {set is_mirror $repo_config(remote.$push_remote.mirror)} +	} +	url {set r_url $push_url} +	} +	if {$r_url eq {}} return + +	set cmd [list git push] +	lappend cmd -v +	if {$push_thin} { +		lappend cmd --thin +	} +	if {$push_force} { +		lappend cmd --force +	} +	if {$push_tags} { +		lappend cmd --tags +	} +	lappend cmd $r_url +	if {$is_mirror} { +		set cons [console::new \ +			[mc "push %s" $r_url] \ +			[mc "Mirroring to %s" $r_url]] +	} else { +		set cnt 0 +		foreach i [$w.source.l curselection] { +			set b [$w.source.l get $i] +			lappend cmd "refs/heads/$b:refs/heads/$b" +			incr cnt +		} +		if {$cnt == 0} { +			return +		} elseif {$cnt == 1} { +			set unit branch +		} else { +			set unit branches +		} + +		set cons [console::new \ +			[mc "push %s" $r_url] \ +			[mc "Pushing %s %s to %s" $cnt $unit $r_url]] +	} +	console::exec $cons $cmd +	destroy $w +} + +trace add variable push_remote write \ +	[list radio_selector push_urltype remote] + +proc do_push_anywhere {} { +	global all_remotes current_branch +	global push_urltype push_remote push_url push_thin push_tags +	global push_force + +	set w .push_setup +	toplevel $w +	catch {wm attributes $w -type dialog} +	wm withdraw $w +	wm geometry $w "+[winfo rootx .]+[winfo rooty .]" +	pave_toplevel $w + +	ttk::label $w.header -text [mc "Push Branches"] \ +		-font font_uibold -anchor center +	pack $w.header -side top -fill x + +	ttk::frame $w.buttons +	ttk::button $w.buttons.create -text [mc Push] \ +		-default active \ +		-command [list start_push_anywhere_action $w] +	pack $w.buttons.create -side right +	ttk::button $w.buttons.cancel -text [mc "Cancel"] \ +		-default normal \ +		-command [list destroy $w] +	pack $w.buttons.cancel -side right -padx 5 +	pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + +	ttk::labelframe $w.source -text [mc "Source Branches"] +	slistbox $w.source.l \ +		-height 10 \ +		-width 70 \ +		-selectmode extended +	foreach h [load_all_heads] { +		$w.source.l insert end $h +		if {$h eq $current_branch} { +			$w.source.l select set end +			$w.source.l yview end +		} +	} +	pack $w.source.l -side left -fill both -expand 1 +	pack $w.source -fill both -expand 1 -pady 5 -padx 5 + +	ttk::labelframe $w.dest -text [mc "Destination Repository"] +	if {$all_remotes ne {}} { +		ttk::radiobutton $w.dest.remote_r \ +			-text [mc "Remote:"] \ +			-value remote \ +			-variable push_urltype +		ttk::combobox $w.dest.remote_m -state readonly \ +			-exportselection false \ +			-textvariable push_remote \ +			-values $all_remotes +		grid $w.dest.remote_r $w.dest.remote_m -sticky w +		if {[lsearch -sorted -exact $all_remotes origin] != -1} { +			set push_remote origin +		} else { +			set push_remote [lindex $all_remotes 0] +		} +		set push_urltype remote +	} else { +		set push_urltype url +	} +	ttk::radiobutton $w.dest.url_r \ +		-text [mc "Arbitrary Location:"] \ +		-value url \ +		-variable push_urltype +	ttk::entry $w.dest.url_t \ +		-width 50 \ +		-textvariable push_url \ +		-validate key \ +		-validatecommand { +			if {%d == 1 && [regexp {\s} %S]} {return 0} +			if {%d == 1 && [string length %S] > 0} { +				set push_urltype url +			} +			return 1 +		} +	grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} +	grid columnconfigure $w.dest 1 -weight 1 +	pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + +	ttk::labelframe $w.options -text [mc "Transfer Options"] +	ttk::checkbutton $w.options.force \ +		-text [mc "Force overwrite existing branch (may discard changes)"] \ +		-variable push_force +	grid $w.options.force -columnspan 2 -sticky w +	ttk::checkbutton $w.options.thin \ +		-text [mc "Use thin pack (for slow network connections)"] \ +		-variable push_thin +	grid $w.options.thin -columnspan 2 -sticky w +	ttk::checkbutton $w.options.tags \ +		-text [mc "Include tags"] \ +		-variable push_tags +	grid $w.options.tags -columnspan 2 -sticky w +	grid columnconfigure $w.options 1 -weight 1 +	pack $w.options -anchor nw -fill x -pady 5 -padx 5 + +	set push_url {} +	set push_force 0 +	set push_thin 0 +	set push_tags 0 + +	bind $w <Visibility> "grab $w; focus $w.buttons.create" +	bind $w <Key-Escape> "destroy $w" +	bind $w <Key-Return> [list start_push_anywhere_action $w] +	wm title $w [mc "%s (%s): Push" [appname] [reponame]] +	wm deiconify $w +	tkwait window $w +} diff --git a/git-gui/lib/win32.tcl b/git-gui/lib/win32.tcl new file mode 100644 index 0000000000..3aedae2f13 --- /dev/null +++ b/git-gui/lib/win32.tcl @@ -0,0 +1,27 @@ +# git-gui Misc. native Windows 32 support +# Copyright (C) 2007 Shawn Pearce + +proc win32_read_lnk {lnk_path} { +	return [safe_exec [list cscript.exe \ +		/E:jscript \ +		/nologo \ +		[file join $::oguilib win32_shortcut.js] \ +		$lnk_path]] +} + +proc win32_create_lnk {lnk_path lnk_exec lnk_dir} { +	global oguilib + +	set lnk_args [lrange $lnk_exec 1 end] +	set lnk_exec [lindex $lnk_exec 0] + +	set cmd [list wscript.exe \ +		/E:jscript \ +		/nologo \ +		[file nativename [file join $oguilib win32_shortcut.js]] \ +		$lnk_path \ +		[file nativename [file join $oguilib git-gui.ico]] \ +		$lnk_dir \ +		$lnk_exec] +	safe_exec [concat $cmd $lnk_args] +} diff --git a/git-gui/lib/win32_shortcut.js b/git-gui/lib/win32_shortcut.js new file mode 100644 index 0000000000..117923f886 --- /dev/null +++ b/git-gui/lib/win32_shortcut.js @@ -0,0 +1,34 @@ +// git-gui Windows shortcut support +// Copyright (C) 2007 Shawn Pearce + +var WshShell = WScript.CreateObject("WScript.Shell"); +var argv = WScript.Arguments; +var argi = 0; +var lnk_path = argv.item(argi++); +var ico_path = argi < argv.length ? argv.item(argi++) : undefined; +var dir_path = argi < argv.length ? argv.item(argi++) : undefined; +var lnk_exec = argi < argv.length ? argv.item(argi++) : undefined; +var lnk_args = ''; +while (argi < argv.length) { +	var s = argv.item(argi++); +	if (lnk_args != '') +		lnk_args += ' '; +	if (s.indexOf(' ') >= 0) { +		lnk_args += '"'; +		lnk_args += s; +		lnk_args += '"'; +	} else { +		lnk_args += s; +	} +} + +var lnk = WshShell.CreateShortcut(lnk_path); +if (argv.length == 1) { +	WScript.echo(lnk.TargetPath); +} else { +	lnk.TargetPath = lnk_exec; +	lnk.Arguments = lnk_args; +	lnk.IconLocation = ico_path + ", 0"; +	lnk.WorkingDirectory = dir_path; +	lnk.Save(); +} | 
