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