1# git-gui commit checkout support
2# Copyright (C) 2007 Shawn Pearce
3
4class checkout_op {
5
6field w        {}; # our window (if we have one)
7field w_cons   {}; # embedded console window object
8
9field new_expr   ; # expression the user saw/thinks this is
10field new_hash   ; # commit SHA-1 we are switching to
11field new_ref    ; # ref we are updating/creating
12field old_hash   ; # commit SHA-1 that was checked out when we started
13
14field parent_w      .; # window that started us
15field merge_type none; # type of merge to apply to existing branch
16field merge_base   {}; # merge base if we have another ref involved
17field fetch_spec   {}; # refetch tracking branch if used?
18field checkout      1; # actually checkout the branch?
19field create        0; # create the branch if it doesn't exist?
20field remote_source {}; # same as fetch_spec, to setup tracking
21
22field reset_ok      0; # did the user agree to reset?
23field fetch_ok      0; # did the fetch succeed?
24
25field readtree_d   {}; # buffered output from read-tree
26field update_old   {}; # was the update-ref call deferred?
27field reflog_msg   {}; # log message for the update-ref call
28
29constructor new {expr hash {ref {}}} {
30	set new_expr $expr
31	set new_hash $hash
32	set new_ref  $ref
33
34	return $this
35}
36
37method parent {path} {
38	set parent_w [winfo toplevel $path]
39}
40
41method enable_merge {type} {
42	set merge_type $type
43}
44
45method enable_fetch {spec} {
46	set fetch_spec $spec
47}
48
49method remote_source {spec} {
50	set remote_source $spec
51}
52
53method enable_checkout {co} {
54	set checkout $co
55}
56
57method enable_create {co} {
58	set create $co
59}
60
61method run {} {
62	if {$fetch_spec ne {}} {
63		global M1B
64
65		# We were asked to refresh a single tracking branch
66		# before we get to work.  We should do that before we
67		# consider any ref updating.
68		#
69		set fetch_ok 0
70		set l_trck [lindex $fetch_spec 0]
71		set remote [lindex $fetch_spec 1]
72		set r_head [lindex $fetch_spec 2]
73		regsub ^refs/heads/ $r_head {} r_name
74
75		set cmd [list git fetch $remote]
76		if {$l_trck ne {}} {
77			lappend cmd +$r_head:$l_trck
78		} else {
79			lappend cmd $r_head
80		}
81
82		_toplevel $this {Refreshing Tracking Branch}
83		set w_cons [::console::embed \
84			$w.console \
85			[mc "Fetching %s from %s" $r_name $remote]]
86		pack $w.console -fill both -expand 1
87		$w_cons exec $cmd [cb _finish_fetch]
88
89		bind $w <$M1B-Key-w> break
90		bind $w <$M1B-Key-W> break
91		bind $w <Visibility> "
92			[list grab $w]
93			[list focus $w]
94		"
95		wm protocol $w WM_DELETE_WINDOW [cb _noop]
96		tkwait window $w
97
98		if {!$fetch_ok} {
99			delete_this
100			return 0
101		}
102	}
103
104	if {$new_ref ne {}} {
105		# If we have a ref we need to update it before we can
106		# proceed with a checkout (if one was enabled).
107		#
108		if {![_update_ref $this]} {
109			delete_this
110			return 0
111		}
112	}
113
114	if {$checkout} {
115		_checkout $this
116		return 1
117	}
118
119	delete_this
120	return 1
121}
122
123method _noop {} {}
124
125method _finish_fetch {ok} {
126	if {$ok} {
127		set l_trck [lindex $fetch_spec 0]
128		if {$l_trck eq {}} {
129			set l_trck FETCH_HEAD
130		}
131		if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
132			set ok 0
133			$w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
134			$w_cons insert $err
135		}
136	}
137
138	$w_cons done $ok
139	set w_cons {}
140	wm protocol $w WM_DELETE_WINDOW {}
141
142	if {$ok} {
143		destroy $w
144		set w {}
145	} else {
146		button $w.close -text [mc Close] -command [list destroy $w]
147		pack $w.close -side bottom -anchor e -padx 10 -pady 10
148	}
149
150	set fetch_ok $ok
151}
152
153method _update_ref {} {
154	global null_sha1 current_branch repo_config
155
156	set ref $new_ref
157	set new $new_hash
158
159	set is_current 0
160	set rh refs/heads/
161	set rn [string length $rh]
162	if {[string equal -length $rn $rh $ref]} {
163		set newbranch [string range $ref $rn end]
164		if {$current_branch eq $newbranch} {
165			set is_current 1
166		}
167	} else {
168		set newbranch $ref
169	}
170
171	if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
172		# Assume it does not exist, and that is what the error was.
173		#
174		if {!$create} {
175			_error $this [mc "Branch '%s' does not exist." $newbranch]
176			return 0
177		}
178
179		set reflog_msg "branch: Created from $new_expr"
180		set cur $null_sha1
181
182		if {($repo_config(branch.autosetupmerge) eq {true}
183			|| $repo_config(branch.autosetupmerge) eq {always})
184			&& $remote_source ne {}
185			&& "refs/heads/$newbranch" eq $ref} {
186
187			set c_remote [lindex $remote_source 1]
188			set c_merge [lindex $remote_source 2]
189			if {[catch {
190					git config branch.$newbranch.remote $c_remote
191					git config branch.$newbranch.merge  $c_merge
192				} err]} {
193				_error $this [strcat \
194				[mc "Failed to configure simplified git-pull for '%s'." $newbranch] \
195				"\n\n$err"]
196			}
197		}
198	} elseif {$create && $merge_type eq {none}} {
199		# We were told to create it, but not do a merge.
200		# Bad.  Name shouldn't have existed.
201		#
202		_error $this [mc "Branch '%s' already exists." $newbranch]
203		return 0
204	} elseif {!$create && $merge_type eq {none}} {
205		# We aren't creating, it exists and we don't merge.
206		# We are probably just a simple branch switch.
207		# Use whatever value we just read.
208		#
209		set new      $cur
210		set new_hash $cur
211	} elseif {$new eq $cur} {
212		# No merge would be required, don't compute anything.
213		#
214	} else {
215		catch {set merge_base [git merge-base $new $cur]}
216		if {$merge_base eq $cur} {
217			# The current branch is older.
218			#
219			set reflog_msg "merge $new_expr: Fast-forward"
220		} else {
221			switch -- $merge_type {
222			ff {
223				if {$merge_base eq $new} {
224					# The current branch is actually newer.
225					#
226					set new $cur
227					set new_hash $cur
228				} else {
229					_error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
230					return 0
231				}
232			}
233			reset {
234				# The current branch will lose things.
235				#
236				if {[_confirm_reset $this $cur]} {
237					set reflog_msg "reset $new_expr"
238				} else {
239					return 0
240				}
241			}
242			default {
243				_error $this [mc "Merge strategy '%s' not supported." $merge_type]
244				return 0
245			}
246			}
247		}
248	}
249
250	if {$new ne $cur} {
251		if {$is_current} {
252			# No so fast.  We should defer this in case
253			# we cannot update the working directory.
254			#
255			set update_old $cur
256			return 1
257		}
258
259		if {[catch {
260				git update-ref -m $reflog_msg $ref $new $cur
261			} err]} {
262			_error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
263			return 0
264		}
265	}
266
267	return 1
268}
269
270method _checkout {} {
271	if {[lock_index checkout_op]} {
272		after idle [cb _start_checkout]
273	} else {
274		_error $this [mc "Staging area (index) is already locked."]
275		delete_this
276	}
277}
278
279method _start_checkout {} {
280	global HEAD commit_type
281
282	# -- Our in memory state should match the repository.
283	#
284	repository_state curType old_hash curMERGE_HEAD
285	if {[string match amend* $commit_type]
286		&& $curType eq {normal}
287		&& $old_hash eq $HEAD} {
288	} elseif {$commit_type ne $curType || $HEAD ne $old_hash} {
289		info_popup [mc "Last scanned state does not match repository state.
290
291Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
292
293The rescan will be automatically started now.
294"]
295		unlock_index
296		rescan ui_ready
297		delete_this
298		return
299	}
300
301	if {$old_hash eq $new_hash} {
302		_after_readtree $this
303	} elseif {[is_config_true gui.trustmtime]} {
304		_readtree $this
305	} else {
306		ui_status [mc "Refreshing file status..."]
307		set fd [git_read update-index \
308			-q \
309			--unmerged \
310			--ignore-missing \
311			--refresh \
312			]
313		fconfigure $fd -blocking 0 -translation binary
314		fileevent $fd readable [cb _refresh_wait $fd]
315	}
316}
317
318method _refresh_wait {fd} {
319	read $fd
320	if {[eof $fd]} {
321		close $fd
322		_readtree $this
323	}
324}
325
326method _name {} {
327	if {$new_ref eq {}} {
328		return [string range $new_hash 0 7]
329	}
330
331	set rh refs/heads/
332	set rn [string length $rh]
333	if {[string equal -length $rn $rh $new_ref]} {
334		return [string range $new_ref $rn end]
335	} else {
336		return $new_ref
337	}
338}
339
340method _readtree {} {
341	global HEAD
342
343	set readtree_d {}
344	set status_bar_operation [$::main_status start \
345		[mc "Updating working directory to '%s'..." [_name $this]] \
346		[mc "files checked out"]]
347
348	set fd [git_read --stderr read-tree \
349		-m \
350		-u \
351		-v \
352		--exclude-per-directory=.gitignore \
353		$HEAD \
354		$new_hash \
355		]
356	fconfigure $fd -blocking 0 -translation binary
357	fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
358}
359
360method _readtree_wait {fd status_bar_operation} {
361	global current_branch
362
363	set buf [read $fd]
364	$status_bar_operation update_meter $buf
365	append readtree_d $buf
366
367	fconfigure $fd -blocking 1
368	if {![eof $fd]} {
369		fconfigure $fd -blocking 0
370		$status_bar_operation stop
371		return
372	}
373
374	if {[catch {close $fd}]} {
375		set err $readtree_d
376		regsub {^fatal: } $err {} err
377		$status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
378		warn_popup [strcat [mc "File level merge required."] "
379
380$err
381
382" [mc "Staying on branch '%s'." $current_branch]]
383		unlock_index
384		delete_this
385		return
386	}
387
388	$status_bar_operation stop
389	_after_readtree $this
390}
391
392method _after_readtree {} {
393	global commit_type HEAD MERGE_HEAD PARENT
394	global current_branch is_detached
395	global ui_comm
396
397	set name [_name $this]
398	set log "checkout: moving"
399	if {!$is_detached} {
400		append log " from $current_branch"
401	}
402
403	# -- Move/create HEAD as a symbolic ref.  Core git does not
404	#    even check for failure here, it Just Works(tm).  If it
405	#    doesn't we are in some really ugly state that is difficult
406	#    to recover from within git-gui.
407	#
408	set rh refs/heads/
409	set rn [string length $rh]
410	if {[string equal -length $rn $rh $new_ref]} {
411		set new_branch [string range $new_ref $rn end]
412		if {$is_detached || $current_branch ne $new_branch} {
413			append log " to $new_branch"
414			if {[catch {
415					git symbolic-ref -m $log HEAD $new_ref
416				} err]} {
417				_fatal $this $err
418			}
419			set current_branch $new_branch
420			set is_detached 0
421		}
422	} else {
423		if {!$is_detached || $new_hash ne $HEAD} {
424			append log " to $new_expr"
425			if {[catch {
426					_detach_HEAD $log $new_hash
427				} err]} {
428				_fatal $this $err
429			}
430		}
431		set current_branch HEAD
432		set is_detached 1
433	}
434
435	# -- We had to defer updating the branch itself until we
436	#    knew the working directory would update.  So now we
437	#    need to finish that work.  If it fails we're in big
438	#    trouble.
439	#
440	if {$update_old ne {}} {
441		if {[catch {
442				git update-ref \
443					-m $reflog_msg \
444					$new_ref \
445					$new_hash \
446					$update_old
447			} err]} {
448			_fatal $this $err
449		}
450	}
451
452	if {$is_detached} {
453		info_popup [mc "You are no longer on a local branch.
454
455If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
456	}
457
458	# -- Run the post-checkout hook.
459	#
460	set fd_ph [githook_read post-checkout $old_hash $new_hash 1]
461	if {$fd_ph ne {}} {
462		global pch_error
463		set pch_error {}
464		fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
465		fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph]
466	} else {
467		_update_repo_state $this
468	}
469}
470
471method _postcheckout_wait {fd_ph} {
472	global pch_error
473
474	append pch_error [read $fd_ph]
475	fconfigure $fd_ph -blocking 1
476	if {[eof $fd_ph]} {
477		if {[catch {close $fd_ph}]} {
478			hook_failed_popup post-checkout $pch_error 0
479		}
480		unset pch_error
481		_update_repo_state $this
482		return
483	}
484	fconfigure $fd_ph -blocking 0
485}
486
487method _update_repo_state {} {
488	# -- Update our repository state.  If we were previously in
489	#    amend mode we need to toss the current buffer and do a
490	#    full rescan to update our file lists.  If we weren't in
491	#    amend mode our file lists are accurate and we can avoid
492	#    the rescan.
493	#
494	global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT
495	global ui_comm
496
497	unlock_index
498	set name [_name $this]
499	set commit_type_is_amend 0
500	if {[string match amend* $commit_type]} {
501		$ui_comm delete 0.0 end
502		$ui_comm edit reset
503		$ui_comm edit modified false
504		rescan [list ui_status [mc "Checked out '%s'." $name]]
505	} else {
506		repository_state commit_type HEAD MERGE_HEAD
507		set PARENT $HEAD
508		ui_status [mc "Checked out '%s'." $name]
509	}
510	delete_this
511}
512
513git-version proc _detach_HEAD {log new} {
514	>= 1.5.3 {
515		git update-ref --no-deref -m $log HEAD $new
516	}
517	default {
518		set p [gitdir HEAD]
519		file delete $p
520		set fd [open $p w]
521		fconfigure $fd -translation lf -encoding utf-8
522		puts $fd $new
523		close $fd
524	}
525}
526
527method _confirm_reset {cur} {
528	set reset_ok 0
529	set name [_name $this]
530	set gitk [list do_gitk [list $cur ^$new_hash]]
531
532	_toplevel $this {Confirm Branch Reset}
533	pack [label $w.msg1 \
534		-anchor w \
535		-justify left \
536		-text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
537		] -anchor w
538
539	set list $w.list.l
540	frame $w.list
541	text $list \
542		-font font_diff \
543		-width 80 \
544		-height 10 \
545		-wrap none \
546		-xscrollcommand [list $w.list.sbx set] \
547		-yscrollcommand [list $w.list.sby set]
548	scrollbar $w.list.sbx -orient h -command [list $list xview]
549	scrollbar $w.list.sby -orient v -command [list $list yview]
550	pack $w.list.sbx -fill x -side bottom
551	pack $w.list.sby -fill y -side right
552	pack $list -fill both -expand 1
553	pack $w.list -fill both -expand 1 -padx 5 -pady 5
554
555	pack [label $w.msg2 \
556		-anchor w \
557		-justify left \
558		-text [mc "Recovering lost commits may not be easy."] \
559		]
560	pack [label $w.msg3 \
561		-anchor w \
562		-justify left \
563		-text [mc "Reset '%s'?" $name] \
564		]
565
566	frame $w.buttons
567	button $w.buttons.visualize \
568		-text [mc Visualize] \
569		-command $gitk
570	pack $w.buttons.visualize -side left
571	button $w.buttons.reset \
572		-text [mc Reset] \
573		-command "
574			set @reset_ok 1
575			destroy $w
576		"
577	pack $w.buttons.reset -side right
578	button $w.buttons.cancel \
579		-default active \
580		-text [mc Cancel] \
581		-command [list destroy $w]
582	pack $w.buttons.cancel -side right -padx 5
583	pack $w.buttons -side bottom -fill x -pady 10 -padx 10
584
585	set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
586	while {[gets $fd line] > 0} {
587		set abbr [string range $line 0 7]
588		set subj [string range $line 41 end]
589		$list insert end "$abbr  $subj\n"
590	}
591	close $fd
592	$list configure -state disabled
593
594	bind $w    <Key-v> $gitk
595	bind $w <Visibility> "
596		grab $w
597		focus $w.buttons.cancel
598	"
599	bind $w <Key-Return> [list destroy $w]
600	bind $w <Key-Escape> [list destroy $w]
601	tkwait window $w
602	return $reset_ok
603}
604
605method _error {msg} {
606	if {[winfo ismapped $parent_w]} {
607		set p $parent_w
608	} else {
609		set p .
610	}
611
612	tk_messageBox \
613		-icon error \
614		-type ok \
615		-title [wm title $p] \
616		-parent $p \
617		-message $msg
618}
619
620method _toplevel {title} {
621	regsub -all {::} $this {__} w
622	set w .$w
623
624	if {[winfo ismapped $parent_w]} {
625		set p $parent_w
626	} else {
627		set p .
628	}
629
630	toplevel $w
631	wm title $w $title
632	wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
633}
634
635method _fatal {err} {
636	error_popup [strcat [mc "Failed to set current branch.
637
638This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
639
640This should not have occurred.  %s will now close and give up." [appname]] "
641
642$err"]
643	exit 1
644}
645
646}
647