1# git-gui misc. commit reading/writing support
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc load_last_commit {} {
5	global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author
6	global repo_config
7
8	if {[llength $PARENT] == 0} {
9		error_popup [mc "There is nothing to amend.
10
11You are about to create the initial commit.  There is no commit before this to amend.
12"]
13		return
14	}
15
16	repository_state curType curHEAD curMERGE_HEAD
17	if {$curType eq {merge}} {
18		error_popup [mc "Cannot amend while merging.
19
20You 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.
21"]
22		return
23	}
24
25	set msg {}
26	set parents [list]
27	if {[catch {
28			set name ""
29			set email ""
30			set fd [git_read cat-file commit $curHEAD]
31			fconfigure $fd -encoding binary -translation lf
32			# By default commits are assumed to be in utf-8
33			set enc utf-8
34			while {[gets $fd line] > 0} {
35				if {[string match {parent *} $line]} {
36					lappend parents [string range $line 7 end]
37				} elseif {[string match {encoding *} $line]} {
38					set enc [string tolower [string range $line 9 end]]
39				} elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} { }
40			}
41			set msg [read $fd]
42			close $fd
43
44			set enc [tcl_encoding $enc]
45			if {$enc ne {}} {
46				set msg [encoding convertfrom $enc $msg]
47				set name [encoding convertfrom $enc $name]
48				set email [encoding convertfrom $enc $email]
49			}
50			if {$name ne {} && $email ne {}} {
51				set commit_author [list name $name email $email date $time]
52			}
53
54			set msg [string trim $msg]
55		} err]} {
56		error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
57		return
58	}
59
60	set HEAD $curHEAD
61	set PARENT $parents
62	set MERGE_HEAD [list]
63	switch -- [llength $parents] {
64	0       {set commit_type amend-initial}
65	1       {set commit_type amend}
66	default {set commit_type amend-merge}
67	}
68
69	$ui_comm delete 0.0 end
70	$ui_comm insert end $msg
71	$ui_comm edit reset
72	$ui_comm edit modified false
73	rescan ui_ready
74}
75
76set GIT_COMMITTER_IDENT {}
77
78proc committer_ident {} {
79	global GIT_COMMITTER_IDENT
80
81	if {$GIT_COMMITTER_IDENT eq {}} {
82		if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
83			error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
84			return {}
85		}
86		if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
87			$me me GIT_COMMITTER_IDENT]} {
88			error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
89			return {}
90		}
91	}
92
93	return $GIT_COMMITTER_IDENT
94}
95
96proc do_signoff {} {
97	global ui_comm
98
99	set me [committer_ident]
100	if {$me eq {}} return
101
102	set sob "Signed-off-by: $me"
103	set last [$ui_comm get {end -1c linestart} {end -1c}]
104	if {$last ne $sob} {
105		$ui_comm edit separator
106		if {$last ne {}
107			&& ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
108			$ui_comm insert end "\n"
109		}
110		$ui_comm insert end "\n$sob"
111		$ui_comm edit separator
112		$ui_comm see end
113	}
114}
115
116proc create_new_commit {} {
117	global commit_type ui_comm commit_author
118
119	set commit_type normal
120	unset -nocomplain commit_author
121	$ui_comm delete 0.0 end
122	$ui_comm edit reset
123	$ui_comm edit modified false
124	rescan ui_ready
125}
126
127proc setup_commit_encoding {msg_wt {quiet 0}} {
128	global repo_config
129
130	if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
131		set enc utf-8
132	}
133	set use_enc [tcl_encoding $enc]
134	if {$use_enc ne {}} {
135		fconfigure $msg_wt -encoding $use_enc
136	} else {
137		if {!$quiet} {
138			error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
139		}
140		fconfigure $msg_wt -encoding utf-8
141	}
142}
143
144proc commit_tree {} {
145	global HEAD commit_type file_states ui_comm repo_config
146	global pch_error
147
148	if {[committer_ident] eq {}} return
149	if {![lock_index update]} return
150
151	# -- Our in memory state should match the repository.
152	#
153	repository_state curType curHEAD curMERGE_HEAD
154	if {[string match amend* $commit_type]
155		&& $curType eq {normal}
156		&& $curHEAD eq $HEAD} {
157	} elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
158		info_popup [mc "Last scanned state does not match repository state.
159
160Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
161
162The rescan will be automatically started now.
163"]
164		unlock_index
165		rescan ui_ready
166		return
167	}
168
169	# -- At least one file should differ in the index.
170	#
171	set files_ready 0
172	foreach path [array names file_states] {
173		set s $file_states($path)
174		switch -glob -- [lindex $s 0] {
175		_? {continue}
176		A? -
177		D? -
178		T? -
179		M? {set files_ready 1}
180		_U -
181		U? {
182			error_popup [mc "Unmerged files cannot be committed.
183
184File %s has merge conflicts.  You must resolve them and stage the file before committing.
185" [short_path $path]]
186			unlock_index
187			return
188		}
189		default {
190			error_popup [mc "Unknown file state %s detected.
191
192File %s cannot be committed by this program.
193" [lindex $s 0] [short_path $path]]
194		}
195		}
196	}
197	if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
198		info_popup [mc "No changes to commit.
199
200You must stage at least 1 file before you can commit.
201"]
202		unlock_index
203		return
204	}
205
206	if {[is_enabled nocommitmsg]} { do_quit 0 }
207
208	# -- A message is required.
209	#
210	set msg [string trim [$ui_comm get 1.0 end]]
211	regsub -all -line {[ \t\r]+$} $msg {} msg
212	if {$msg eq {}} {
213		error_popup [mc "Please supply a commit message.
214
215A good commit message has the following format:
216
217- First line: Describe in one sentence what you did.
218- Second line: Blank
219- Remaining lines: Describe why this change is good.
220"]
221		unlock_index
222		return
223	}
224
225	# -- Build the message file.
226	#
227	set msg_p [gitdir GITGUI_EDITMSG]
228	set msg_wt [open $msg_p w]
229	fconfigure $msg_wt -translation lf
230	setup_commit_encoding $msg_wt
231	puts $msg_wt $msg
232	close $msg_wt
233
234	if {[is_enabled nocommit]} { do_quit 0 }
235
236	# -- Run the pre-commit hook.
237	#
238	set fd_ph [githook_read pre-commit]
239	if {$fd_ph eq {}} {
240		commit_commitmsg $curHEAD $msg_p
241		return
242	}
243
244	ui_status [mc "Calling pre-commit hook..."]
245	set pch_error {}
246	fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
247	fileevent $fd_ph readable \
248		[list commit_prehook_wait $fd_ph $curHEAD $msg_p]
249}
250
251proc commit_prehook_wait {fd_ph curHEAD msg_p} {
252	global pch_error
253
254	append pch_error [read $fd_ph]
255	fconfigure $fd_ph -blocking 1
256	if {[eof $fd_ph]} {
257		if {[catch {close $fd_ph}]} {
258			catch {file delete $msg_p}
259			ui_status [mc "Commit declined by pre-commit hook."]
260			hook_failed_popup pre-commit $pch_error
261			unlock_index
262		} else {
263			commit_commitmsg $curHEAD $msg_p
264		}
265		set pch_error {}
266		return
267	}
268	fconfigure $fd_ph -blocking 0
269}
270
271proc commit_commitmsg {curHEAD msg_p} {
272	global is_detached repo_config
273	global pch_error
274
275	if {$is_detached
276	    && ![file exists [gitdir rebase-merge head-name]]
277	    && 	[is_config_true gui.warndetachedcommit]} {
278		set msg [mc "You are about to commit on a detached head.\
279This is a potentially dangerous thing to do because if you switch\
280to another branch you will lose your changes and it can be difficult\
281to retrieve them later from the reflog. You should probably cancel this\
282commit and create a new branch to continue.\n\
283\n\
284Do you really want to proceed with your Commit?"]
285		if {[ask_popup $msg] ne yes} {
286			unlock_index
287			return
288		}
289	}
290
291	# -- Run the commit-msg hook.
292	#
293	set fd_ph [githook_read commit-msg $msg_p]
294	if {$fd_ph eq {}} {
295		commit_writetree $curHEAD $msg_p
296		return
297	}
298
299	ui_status [mc "Calling commit-msg hook..."]
300	set pch_error {}
301	fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
302	fileevent $fd_ph readable \
303		[list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
304}
305
306proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
307	global pch_error
308
309	append pch_error [read $fd_ph]
310	fconfigure $fd_ph -blocking 1
311	if {[eof $fd_ph]} {
312		if {[catch {close $fd_ph}]} {
313			catch {file delete $msg_p}
314			ui_status [mc "Commit declined by commit-msg hook."]
315			hook_failed_popup commit-msg $pch_error
316			unlock_index
317		} else {
318			commit_writetree $curHEAD $msg_p
319		}
320		set pch_error {}
321		return
322	}
323	fconfigure $fd_ph -blocking 0
324}
325
326proc commit_writetree {curHEAD msg_p} {
327	ui_status [mc "Committing changes..."]
328	set fd_wt [git_read write-tree]
329	fileevent $fd_wt readable \
330		[list commit_committree $fd_wt $curHEAD $msg_p]
331}
332
333proc commit_committree {fd_wt curHEAD msg_p} {
334	global HEAD PARENT MERGE_HEAD commit_type commit_author
335	global current_branch
336	global ui_comm commit_type_is_amend
337	global file_states selected_paths rescan_active
338	global repo_config
339	global env
340
341	gets $fd_wt tree_id
342	if {[catch {close $fd_wt} err]} {
343		catch {file delete $msg_p}
344		error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
345		ui_status [mc "Commit failed."]
346		unlock_index
347		return
348	}
349
350	# -- Verify this wasn't an empty change.
351	#
352	if {$commit_type eq {normal}} {
353		set fd_ot [git_read cat-file commit $PARENT]
354		fconfigure $fd_ot -encoding binary -translation lf
355		set old_tree [gets $fd_ot]
356		close $fd_ot
357
358		if {[string equal -length 5 {tree } $old_tree]
359			&& [string length $old_tree] == 45} {
360			set old_tree [string range $old_tree 5 end]
361		} else {
362			error [mc "Commit %s appears to be corrupt" $PARENT]
363		}
364
365		if {$tree_id eq $old_tree} {
366			catch {file delete $msg_p}
367			info_popup [mc "No changes to commit.
368
369No files were modified by this commit and it was not a merge commit.
370
371A rescan will be automatically started now.
372"]
373			unlock_index
374			rescan {ui_status [mc "No changes to commit."]}
375			return
376		}
377	}
378
379	if {[info exists commit_author]} {
380		set old_author [commit_author_ident $commit_author]
381	}
382	# -- Create the commit.
383	#
384	set cmd [list commit-tree $tree_id]
385	if {[is_config_true commit.gpgsign]} {
386		lappend cmd -S
387	}
388	foreach p [concat $PARENT $MERGE_HEAD] {
389		lappend cmd -p $p
390	}
391	lappend cmd <$msg_p
392	if {[catch {set cmt_id [eval git $cmd]} err]} {
393		catch {file delete $msg_p}
394		error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
395		ui_status [mc "Commit failed."]
396		unlock_index
397		unset -nocomplain commit_author
398		commit_author_reset $old_author
399		return
400	}
401	if {[info exists commit_author]} {
402		unset -nocomplain commit_author
403		commit_author_reset $old_author
404	}
405
406	# -- Update the HEAD ref.
407	#
408	set reflogm commit
409	if {$commit_type ne {normal}} {
410		append reflogm " ($commit_type)"
411	}
412	set msg_fd [open $msg_p r]
413	setup_commit_encoding $msg_fd 1
414	gets $msg_fd subject
415	close $msg_fd
416	append reflogm {: } $subject
417	if {[catch {
418			git update-ref -m $reflogm HEAD $cmt_id $curHEAD
419		} err]} {
420		catch {file delete $msg_p}
421		error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
422		ui_status [mc "Commit failed."]
423		unlock_index
424		return
425	}
426
427	# -- Cleanup after ourselves.
428	#
429	catch {file delete $msg_p}
430	catch {file delete [gitdir MERGE_HEAD]}
431	catch {file delete [gitdir MERGE_MSG]}
432	catch {file delete [gitdir SQUASH_MSG]}
433	catch {file delete [gitdir GITGUI_MSG]}
434	catch {file delete [gitdir CHERRY_PICK_HEAD]}
435
436	# -- Let rerere do its thing.
437	#
438	if {[get_config rerere.enabled] eq {}} {
439		set rerere [file isdirectory [gitdir rr-cache]]
440	} else {
441		set rerere [is_config_true rerere.enabled]
442	}
443	if {$rerere} {
444		catch {git rerere}
445	}
446
447	# -- Run the post-commit hook.
448	#
449	set fd_ph [githook_read post-commit]
450	if {$fd_ph ne {}} {
451		global pch_error
452		set pch_error {}
453		fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
454		fileevent $fd_ph readable \
455			[list commit_postcommit_wait $fd_ph $cmt_id]
456	}
457
458	$ui_comm delete 0.0 end
459	load_message [get_config commit.template]
460	$ui_comm edit reset
461	$ui_comm edit modified false
462	if {$::GITGUI_BCK_exists} {
463		catch {file delete [gitdir GITGUI_BCK]}
464		set ::GITGUI_BCK_exists 0
465	}
466
467	if {[is_enabled singlecommit]} { do_quit 0 }
468
469	# -- Update in memory status
470	#
471	set commit_type normal
472	set commit_type_is_amend 0
473	set HEAD $cmt_id
474	set PARENT $cmt_id
475	set MERGE_HEAD [list]
476
477	foreach path [array names file_states] {
478		set s $file_states($path)
479		set m [lindex $s 0]
480		switch -glob -- $m {
481		_O -
482		_M -
483		_D {continue}
484		__ -
485		A_ -
486		M_ -
487		T_ -
488		D_ {
489			unset file_states($path)
490			catch {unset selected_paths($path)}
491		}
492		DO {
493			set file_states($path) [list _O [lindex $s 1] {} {}]
494		}
495		AM -
496		AD -
497		AT -
498		TM -
499		TD -
500		MM -
501		MT -
502		MD {
503			set file_states($path) [list \
504				_[string index $m 1] \
505				[lindex $s 1] \
506				[lindex $s 3] \
507				{}]
508		}
509		}
510	}
511
512	display_all_files
513	unlock_index
514	reshow_diff
515	ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
516}
517
518proc commit_postcommit_wait {fd_ph cmt_id} {
519	global pch_error
520
521	append pch_error [read $fd_ph]
522	fconfigure $fd_ph -blocking 1
523	if {[eof $fd_ph]} {
524		if {[catch {close $fd_ph}]} {
525			hook_failed_popup post-commit $pch_error 0
526		}
527		unset pch_error
528		return
529	}
530	fconfigure $fd_ph -blocking 0
531}
532
533proc commit_author_ident {details} {
534	global env
535	array set author $details
536	set old [array get env GIT_AUTHOR_*]
537	set env(GIT_AUTHOR_NAME) $author(name)
538	set env(GIT_AUTHOR_EMAIL) $author(email)
539	set env(GIT_AUTHOR_DATE) $author(date)
540	return $old
541}
542proc commit_author_reset {details} {
543	global env
544	unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
545	if {$details ne {}} {
546		array set env $details
547	}
548}
549