git-gui: make config gui.warndetachedcommit a boolean
[git/dscho.git] / lib / commit.tcl
blobe27e148803906d867844c3adf5fd6e7b6544027c
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc load_last_commit {} {
5 global HEAD PARENT MERGE_HEAD commit_type ui_comm
6 global repo_config
8 if {[llength $PARENT] == 0} {
9 error_popup [mc "There is nothing to amend.
11 You are about to create the initial commit. There is no commit before this to amend.
13 return
16 repository_state curType curHEAD curMERGE_HEAD
17 if {$curType eq {merge}} {
18 error_popup [mc "Cannot amend while merging.
20 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.
22 return
25 set msg {}
26 set parents [list]
27 if {[catch {
28 set fd [git_read cat-file commit $curHEAD]
29 fconfigure $fd -encoding binary -translation lf
30 # By default commits are assumed to be in utf-8
31 set enc utf-8
32 while {[gets $fd line] > 0} {
33 if {[string match {parent *} $line]} {
34 lappend parents [string range $line 7 end]
35 } elseif {[string match {encoding *} $line]} {
36 set enc [string tolower [string range $line 9 end]]
39 set msg [read $fd]
40 close $fd
42 set enc [tcl_encoding $enc]
43 if {$enc ne {}} {
44 set msg [encoding convertfrom $enc $msg]
46 set msg [string trim $msg]
47 } err]} {
48 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
49 return
52 set HEAD $curHEAD
53 set PARENT $parents
54 set MERGE_HEAD [list]
55 switch -- [llength $parents] {
56 0 {set commit_type amend-initial}
57 1 {set commit_type amend}
58 default {set commit_type amend-merge}
61 $ui_comm delete 0.0 end
62 $ui_comm insert end $msg
63 $ui_comm edit reset
64 $ui_comm edit modified false
65 rescan ui_ready
68 set GIT_COMMITTER_IDENT {}
70 proc committer_ident {} {
71 global GIT_COMMITTER_IDENT
73 if {$GIT_COMMITTER_IDENT eq {}} {
74 if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
75 error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
76 return {}
78 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
79 $me me GIT_COMMITTER_IDENT]} {
80 error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
81 return {}
85 return $GIT_COMMITTER_IDENT
88 proc do_signoff {} {
89 global ui_comm
91 set me [committer_ident]
92 if {$me eq {}} return
94 set sob "Signed-off-by: $me"
95 set last [$ui_comm get {end -1c linestart} {end -1c}]
96 if {$last ne $sob} {
97 $ui_comm edit separator
98 if {$last ne {}
99 && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
100 $ui_comm insert end "\n"
102 $ui_comm insert end "\n$sob"
103 $ui_comm edit separator
104 $ui_comm see end
108 proc create_new_commit {} {
109 global commit_type ui_comm
111 set commit_type normal
112 $ui_comm delete 0.0 end
113 $ui_comm edit reset
114 $ui_comm edit modified false
115 rescan ui_ready
118 proc setup_commit_encoding {msg_wt {quiet 0}} {
119 global repo_config
121 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
122 set enc utf-8
124 set use_enc [tcl_encoding $enc]
125 if {$use_enc ne {}} {
126 fconfigure $msg_wt -encoding $use_enc
127 } else {
128 if {!$quiet} {
129 error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
131 fconfigure $msg_wt -encoding utf-8
135 proc commit_tree {} {
136 global HEAD commit_type file_states ui_comm repo_config
137 global pch_error
139 if {[committer_ident] eq {}} return
140 if {![lock_index update]} return
142 # -- Our in memory state should match the repository.
144 repository_state curType curHEAD curMERGE_HEAD
145 if {[string match amend* $commit_type]
146 && $curType eq {normal}
147 && $curHEAD eq $HEAD} {
148 } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
149 info_popup [mc "Last scanned state does not match repository state.
151 Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
153 The rescan will be automatically started now.
155 unlock_index
156 rescan ui_ready
157 return
160 # -- At least one file should differ in the index.
162 set files_ready 0
163 foreach path [array names file_states] {
164 set s $file_states($path)
165 switch -glob -- [lindex $s 0] {
166 _? {continue}
167 A? -
168 D? -
169 T? -
170 M? {set files_ready 1}
171 _U -
172 U? {
173 error_popup [mc "Unmerged files cannot be committed.
175 File %s has merge conflicts. You must resolve them and stage the file before committing.
176 " [short_path $path]]
177 unlock_index
178 return
180 default {
181 error_popup [mc "Unknown file state %s detected.
183 File %s cannot be committed by this program.
184 " [lindex $s 0] [short_path $path]]
188 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
189 info_popup [mc "No changes to commit.
191 You must stage at least 1 file before you can commit.
193 unlock_index
194 return
197 if {[is_enabled nocommitmsg]} { do_quit 0 }
199 # -- A message is required.
201 set msg [string trim [$ui_comm get 1.0 end]]
202 regsub -all -line {[ \t\r]+$} $msg {} msg
203 if {$msg eq {}} {
204 error_popup [mc "Please supply a commit message.
206 A good commit message has the following format:
208 - First line: Describe in one sentence what you did.
209 - Second line: Blank
210 - Remaining lines: Describe why this change is good.
212 unlock_index
213 return
216 # -- Build the message file.
218 set msg_p [gitdir GITGUI_EDITMSG]
219 set msg_wt [open $msg_p w]
220 fconfigure $msg_wt -translation lf
221 setup_commit_encoding $msg_wt
222 puts $msg_wt $msg
223 close $msg_wt
225 if {[is_enabled nocommit]} { do_quit 0 }
227 # -- Run the pre-commit hook.
229 set fd_ph [githook_read pre-commit]
230 if {$fd_ph eq {}} {
231 commit_commitmsg $curHEAD $msg_p
232 return
235 ui_status [mc "Calling pre-commit hook..."]
236 set pch_error {}
237 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
238 fileevent $fd_ph readable \
239 [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
242 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
243 global pch_error
245 append pch_error [read $fd_ph]
246 fconfigure $fd_ph -blocking 1
247 if {[eof $fd_ph]} {
248 if {[catch {close $fd_ph}]} {
249 catch {file delete $msg_p}
250 ui_status [mc "Commit declined by pre-commit hook."]
251 hook_failed_popup pre-commit $pch_error
252 unlock_index
253 } else {
254 commit_commitmsg $curHEAD $msg_p
256 set pch_error {}
257 return
259 fconfigure $fd_ph -blocking 0
262 proc commit_commitmsg {curHEAD msg_p} {
263 global is_detached repo_config
264 global pch_error
266 if {$is_detached && [is_config_true gui.warndetachedcommit]} {
267 set msg [mc "You are about to commit on a detached head.\
268 This is a potentially dangerous thing to do because if you switch\
269 to another branch you will loose your changes and it can be difficult\
270 to retrieve them later from the reflog. You should probably cancel this\
271 commit and create a new branch to continue.\n\
273 Do you really want to proceed with your Commit?"]
274 if {[ask_popup $msg] ne yes} {
275 unlock_index
276 return
280 # -- Run the commit-msg hook.
282 set fd_ph [githook_read commit-msg $msg_p]
283 if {$fd_ph eq {}} {
284 commit_writetree $curHEAD $msg_p
285 return
288 ui_status [mc "Calling commit-msg hook..."]
289 set pch_error {}
290 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
291 fileevent $fd_ph readable \
292 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
295 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
296 global pch_error
298 append pch_error [read $fd_ph]
299 fconfigure $fd_ph -blocking 1
300 if {[eof $fd_ph]} {
301 if {[catch {close $fd_ph}]} {
302 catch {file delete $msg_p}
303 ui_status [mc "Commit declined by commit-msg hook."]
304 hook_failed_popup commit-msg $pch_error
305 unlock_index
306 } else {
307 commit_writetree $curHEAD $msg_p
309 set pch_error {}
310 return
312 fconfigure $fd_ph -blocking 0
315 proc commit_writetree {curHEAD msg_p} {
316 ui_status [mc "Committing changes..."]
317 set fd_wt [git_read write-tree]
318 fileevent $fd_wt readable \
319 [list commit_committree $fd_wt $curHEAD $msg_p]
322 proc commit_committree {fd_wt curHEAD msg_p} {
323 global HEAD PARENT MERGE_HEAD commit_type
324 global current_branch
325 global ui_comm selected_commit_type
326 global file_states selected_paths rescan_active
327 global repo_config
329 gets $fd_wt tree_id
330 if {[catch {close $fd_wt} err]} {
331 catch {file delete $msg_p}
332 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
333 ui_status [mc "Commit failed."]
334 unlock_index
335 return
338 # -- Verify this wasn't an empty change.
340 if {$commit_type eq {normal}} {
341 set fd_ot [git_read cat-file commit $PARENT]
342 fconfigure $fd_ot -encoding binary -translation lf
343 set old_tree [gets $fd_ot]
344 close $fd_ot
346 if {[string equal -length 5 {tree } $old_tree]
347 && [string length $old_tree] == 45} {
348 set old_tree [string range $old_tree 5 end]
349 } else {
350 error [mc "Commit %s appears to be corrupt" $PARENT]
353 if {$tree_id eq $old_tree} {
354 catch {file delete $msg_p}
355 info_popup [mc "No changes to commit.
357 No files were modified by this commit and it was not a merge commit.
359 A rescan will be automatically started now.
361 unlock_index
362 rescan {ui_status [mc "No changes to commit."]}
363 return
367 # -- Create the commit.
369 set cmd [list commit-tree $tree_id]
370 foreach p [concat $PARENT $MERGE_HEAD] {
371 lappend cmd -p $p
373 lappend cmd <$msg_p
374 if {[catch {set cmt_id [eval git $cmd]} err]} {
375 catch {file delete $msg_p}
376 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
377 ui_status [mc "Commit failed."]
378 unlock_index
379 return
382 # -- Update the HEAD ref.
384 set reflogm commit
385 if {$commit_type ne {normal}} {
386 append reflogm " ($commit_type)"
388 set msg_fd [open $msg_p r]
389 setup_commit_encoding $msg_fd 1
390 gets $msg_fd subject
391 close $msg_fd
392 append reflogm {: } $subject
393 if {[catch {
394 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
395 } err]} {
396 catch {file delete $msg_p}
397 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
398 ui_status [mc "Commit failed."]
399 unlock_index
400 return
403 # -- Cleanup after ourselves.
405 catch {file delete $msg_p}
406 catch {file delete [gitdir MERGE_HEAD]}
407 catch {file delete [gitdir MERGE_MSG]}
408 catch {file delete [gitdir SQUASH_MSG]}
409 catch {file delete [gitdir GITGUI_MSG]}
411 # -- Let rerere do its thing.
413 if {[get_config rerere.enabled] eq {}} {
414 set rerere [file isdirectory [gitdir rr-cache]]
415 } else {
416 set rerere [is_config_true rerere.enabled]
418 if {$rerere} {
419 catch {git rerere}
422 # -- Run the post-commit hook.
424 set fd_ph [githook_read post-commit]
425 if {$fd_ph ne {}} {
426 global pch_error
427 set pch_error {}
428 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
429 fileevent $fd_ph readable \
430 [list commit_postcommit_wait $fd_ph $cmt_id]
433 $ui_comm delete 0.0 end
434 $ui_comm edit reset
435 $ui_comm edit modified false
436 if {$::GITGUI_BCK_exists} {
437 catch {file delete [gitdir GITGUI_BCK]}
438 set ::GITGUI_BCK_exists 0
441 if {[is_enabled singlecommit]} { do_quit 0 }
443 # -- Update in memory status
445 set selected_commit_type new
446 set commit_type normal
447 set HEAD $cmt_id
448 set PARENT $cmt_id
449 set MERGE_HEAD [list]
451 foreach path [array names file_states] {
452 set s $file_states($path)
453 set m [lindex $s 0]
454 switch -glob -- $m {
455 _O -
456 _M -
457 _D {continue}
458 __ -
459 A_ -
460 M_ -
461 T_ -
462 D_ {
463 unset file_states($path)
464 catch {unset selected_paths($path)}
466 DO {
467 set file_states($path) [list _O [lindex $s 1] {} {}]
469 AM -
470 AD -
471 AT -
472 TM -
473 TD -
474 MM -
475 MT -
476 MD {
477 set file_states($path) [list \
478 _[string index $m 1] \
479 [lindex $s 1] \
480 [lindex $s 3] \
486 display_all_files
487 unlock_index
488 reshow_diff
489 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
492 proc commit_postcommit_wait {fd_ph cmt_id} {
493 global pch_error
495 append pch_error [read $fd_ph]
496 fconfigure $fd_ph -blocking 1
497 if {[eof $fd_ph]} {
498 if {[catch {close $fd_ph}]} {
499 hook_failed_popup post-commit $pch_error 0
501 unset pch_error
502 return
504 fconfigure $fd_ph -blocking 0