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
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 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]]
37 }
38 }
39 set msg [read $fd]
40 close $fd
41
42 set enc [tcl_encoding $enc]
43 if {$enc ne {}} {
44 set msg [encoding convertfrom $enc $msg]
45 }
46 set msg [string trim $msg]
47 } err]} {
48 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
49 return
50 }
51
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}
59 }
60
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
66}
67
68set GIT_COMMITTER_IDENT {}
69
70proc committer_ident {} {
71 global GIT_COMMITTER_IDENT
72
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 {}
77 }
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 {}
82 }
83 }
84
85 return $GIT_COMMITTER_IDENT
86}
87
88proc do_signoff {} {
89 global ui_comm
90
91 set me [committer_ident]
92 if {$me eq {}} return
93
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"
101 }
102 $ui_comm insert end "\n$sob"
103 $ui_comm edit separator
104 $ui_comm see end
105 }
106}
107
108proc create_new_commit {} {
109 global commit_type ui_comm
110
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
116}
117
118proc setup_commit_encoding {msg_wt {quiet 0}} {
119 global repo_config
120
121 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
122 set enc utf-8
123 }
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]
130 }
131 fconfigure $msg_wt -encoding utf-8
132 }
133}
134
135proc commit_tree {} {
136 global HEAD commit_type file_states ui_comm repo_config
137 global pch_error
138
139 if {[committer_ident] eq {}} return
140 if {![lock_index update]} return
141
142 # -- Our in memory state should match the repository.
143 #
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.
150
151Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
152
153The rescan will be automatically started now.
154"]
155 unlock_index
156 rescan ui_ready
157 return
158 }
159
160 # -- At least one file should differ in the index.
161 #
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.
174
175File %s has merge conflicts. You must resolve them and stage the file before committing.
176" [short_path $path]]
177 unlock_index
178 return
179 }
180 default {
181 error_popup [mc "Unknown file state %s detected.
182
183File %s cannot be committed by this program.
184" [lindex $s 0] [short_path $path]]
185 }
186 }
187 }
188 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
189 info_popup [mc "No changes to commit.
190
191You must stage at least 1 file before you can commit.
192"]
193 unlock_index
194 return
195 }
196
197 if {[is_enabled nocommitmsg]} { do_quit 0 }
198
199 # -- A message is required.
200 #
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.
205
206A good commit message has the following format:
207
208- First line: Describe in one sentence what you did.
209- Second line: Blank
210- Remaining lines: Describe why this change is good.
211"]
212 unlock_index
213 return
214 }
215
216 # -- Build the message file.
217 #
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
224
225 if {[is_enabled nocommit]} { do_quit 0 }
226
227 # -- Run the pre-commit hook.
228 #
229 set fd_ph [githook_read pre-commit]
230 if {$fd_ph eq {}} {
231 commit_commitmsg $curHEAD $msg_p
232 return
233 }
234
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]
240}
241
242proc commit_prehook_wait {fd_ph curHEAD msg_p} {
243 global pch_error
244
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
255 }
256 set pch_error {}
257 return
258 }
259 fconfigure $fd_ph -blocking 0
260}
261
262proc commit_commitmsg {curHEAD msg_p} {
263 global is_detached repo_config
264 global pch_error
265
266 if {$is_detached
267 && ![file exists [gitdir rebase-merge head-name]]
268 && [is_config_true gui.warndetachedcommit]} {
269 set msg [mc "You are about to commit on a detached head.\
270This is a potentially dangerous thing to do because if you switch\
271to another branch you will lose your changes and it can be difficult\
272to retrieve them later from the reflog. You should probably cancel this\
273commit and create a new branch to continue.\n\
274\n\
275Do you really want to proceed with your Commit?"]
276 if {[ask_popup $msg] ne yes} {
277 unlock_index
278 return
279 }
280 }
281
282 # -- Run the commit-msg hook.
283 #
284 set fd_ph [githook_read commit-msg $msg_p]
285 if {$fd_ph eq {}} {
286 commit_writetree $curHEAD $msg_p
287 return
288 }
289
290 ui_status [mc "Calling commit-msg hook..."]
291 set pch_error {}
292 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
293 fileevent $fd_ph readable \
294 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
295}
296
297proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
298 global pch_error
299
300 append pch_error [read $fd_ph]
301 fconfigure $fd_ph -blocking 1
302 if {[eof $fd_ph]} {
303 if {[catch {close $fd_ph}]} {
304 catch {file delete $msg_p}
305 ui_status [mc "Commit declined by commit-msg hook."]
306 hook_failed_popup commit-msg $pch_error
307 unlock_index
308 } else {
309 commit_writetree $curHEAD $msg_p
310 }
311 set pch_error {}
312 return
313 }
314 fconfigure $fd_ph -blocking 0
315}
316
317proc commit_writetree {curHEAD msg_p} {
318 ui_status [mc "Committing changes..."]
319 set fd_wt [git_read write-tree]
320 fileevent $fd_wt readable \
321 [list commit_committree $fd_wt $curHEAD $msg_p]
322}
323
324proc commit_committree {fd_wt curHEAD msg_p} {
325 global HEAD PARENT MERGE_HEAD commit_type
326 global current_branch
327 global ui_comm selected_commit_type
328 global file_states selected_paths rescan_active
329 global repo_config
330
331 gets $fd_wt tree_id
332 if {[catch {close $fd_wt} err]} {
333 catch {file delete $msg_p}
334 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
335 ui_status [mc "Commit failed."]
336 unlock_index
337 return
338 }
339
340 # -- Verify this wasn't an empty change.
341 #
342 if {$commit_type eq {normal}} {
343 set fd_ot [git_read cat-file commit $PARENT]
344 fconfigure $fd_ot -encoding binary -translation lf
345 set old_tree [gets $fd_ot]
346 close $fd_ot
347
348 if {[string equal -length 5 {tree } $old_tree]
349 && [string length $old_tree] == 45} {
350 set old_tree [string range $old_tree 5 end]
351 } else {
352 error [mc "Commit %s appears to be corrupt" $PARENT]
353 }
354
355 if {$tree_id eq $old_tree} {
356 catch {file delete $msg_p}
357 info_popup [mc "No changes to commit.
358
359No files were modified by this commit and it was not a merge commit.
360
361A rescan will be automatically started now.
362"]
363 unlock_index
364 rescan {ui_status [mc "No changes to commit."]}
365 return
366 }
367 }
368
369 # -- Create the commit.
370 #
371 set cmd [list commit-tree $tree_id]
372 if {[is_config_true commit.gpgsign]} {
373 lappend cmd -S
374 }
375 foreach p [concat $PARENT $MERGE_HEAD] {
376 lappend cmd -p $p
377 }
378 lappend cmd <$msg_p
379 if {[catch {set cmt_id [eval git $cmd]} err]} {
380 catch {file delete $msg_p}
381 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
382 ui_status [mc "Commit failed."]
383 unlock_index
384 return
385 }
386
387 # -- Update the HEAD ref.
388 #
389 set reflogm commit
390 if {$commit_type ne {normal}} {
391 append reflogm " ($commit_type)"
392 }
393 set msg_fd [open $msg_p r]
394 setup_commit_encoding $msg_fd 1
395 gets $msg_fd subject
396 close $msg_fd
397 append reflogm {: } $subject
398 if {[catch {
399 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
400 } err]} {
401 catch {file delete $msg_p}
402 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
403 ui_status [mc "Commit failed."]
404 unlock_index
405 return
406 }
407
408 # -- Cleanup after ourselves.
409 #
410 catch {file delete $msg_p}
411 catch {file delete [gitdir MERGE_HEAD]}
412 catch {file delete [gitdir MERGE_MSG]}
413 catch {file delete [gitdir SQUASH_MSG]}
414 catch {file delete [gitdir GITGUI_MSG]}
415 catch {file delete [gitdir CHERRY_PICK_HEAD]}
416
417 # -- Let rerere do its thing.
418 #
419 if {[get_config rerere.enabled] eq {}} {
420 set rerere [file isdirectory [gitdir rr-cache]]
421 } else {
422 set rerere [is_config_true rerere.enabled]
423 }
424 if {$rerere} {
425 catch {git rerere}
426 }
427
428 # -- Run the post-commit hook.
429 #
430 set fd_ph [githook_read post-commit]
431 if {$fd_ph ne {}} {
432 global pch_error
433 set pch_error {}
434 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
435 fileevent $fd_ph readable \
436 [list commit_postcommit_wait $fd_ph $cmt_id]
437 }
438
439 $ui_comm delete 0.0 end
440 $ui_comm edit reset
441 $ui_comm edit modified false
442 if {$::GITGUI_BCK_exists} {
443 catch {file delete [gitdir GITGUI_BCK]}
444 set ::GITGUI_BCK_exists 0
445 }
446
447 if {[is_enabled singlecommit]} { do_quit 0 }
448
449 # -- Update in memory status
450 #
451 set selected_commit_type new
452 set commit_type normal
453 set HEAD $cmt_id
454 set PARENT $cmt_id
455 set MERGE_HEAD [list]
456
457 foreach path [array names file_states] {
458 set s $file_states($path)
459 set m [lindex $s 0]
460 switch -glob -- $m {
461 _O -
462 _M -
463 _D {continue}
464 __ -
465 A_ -
466 M_ -
467 T_ -
468 D_ {
469 unset file_states($path)
470 catch {unset selected_paths($path)}
471 }
472 DO {
473 set file_states($path) [list _O [lindex $s 1] {} {}]
474 }
475 AM -
476 AD -
477 AT -
478 TM -
479 TD -
480 MM -
481 MT -
482 MD {
483 set file_states($path) [list \
484 _[string index $m 1] \
485 [lindex $s 1] \
486 [lindex $s 3] \
487 {}]
488 }
489 }
490 }
491
492 display_all_files
493 unlock_index
494 reshow_diff
495 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
496}
497
498proc commit_postcommit_wait {fd_ph cmt_id} {
499 global pch_error
500
501 append pch_error [read $fd_ph]
502 fconfigure $fd_ph -blocking 1
503 if {[eof $fd_ph]} {
504 if {[catch {close $fd_ph}]} {
505 hook_failed_popup post-commit $pch_error 0
506 }
507 unset pch_error
508 return
509 }
510 fconfigure $fd_ph -blocking 0
511}