show-ref: migrate to parse-options
[git] / git-gui / lib / commit.tcl
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 proc 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
11 You 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
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.
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
68 set GIT_COMMITTER_IDENT {}
69
70 proc 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
88 proc 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
108 proc 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
118 proc 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
135 proc 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
151 Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
152
153 The 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                 switch -glob -- [lindex $file_states($path) 0] {
165                 _? {continue}
166                 A? -
167                 D? -
168                 T_ -
169                 M? {set files_ready 1}
170                 _U -
171                 U? {
172                         error_popup [mc "Unmerged files cannot be committed.
173
174 File %s has merge conflicts.  You must resolve them and stage the file before committing.
175 " [short_path $path]]
176                         unlock_index
177                         return
178                 }
179                 default {
180                         error_popup [mc "Unknown file state %s detected.
181
182 File %s cannot be committed by this program.
183 " [lindex $s 0] [short_path $path]]
184                 }
185                 }
186         }
187         if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
188                 info_popup [mc "No changes to commit.
189
190 You must stage at least 1 file before you can commit.
191 "]
192                 unlock_index
193                 return
194         }
195
196         if {[is_enabled nocommitmsg]} { do_quit 0 }
197
198         # -- A message is required.
199         #
200         set msg [string trim [$ui_comm get 1.0 end]]
201         regsub -all -line {[ \t\r]+$} $msg {} msg
202         if {$msg eq {}} {
203                 error_popup [mc "Please supply a commit message.
204
205 A good commit message has the following format:
206
207 - First line: Describe in one sentence what you did.
208 - Second line: Blank
209 - Remaining lines: Describe why this change is good.
210 "]
211                 unlock_index
212                 return
213         }
214
215         # -- Build the message file.
216         #
217         set msg_p [gitdir GITGUI_EDITMSG]
218         set msg_wt [open $msg_p w]
219         fconfigure $msg_wt -translation lf
220         setup_commit_encoding $msg_wt
221         puts $msg_wt $msg
222         close $msg_wt
223
224         if {[is_enabled nocommit]} { do_quit 0 }
225
226         # -- Run the pre-commit hook.
227         #
228         set fd_ph [githook_read pre-commit]
229         if {$fd_ph eq {}} {
230                 commit_commitmsg $curHEAD $msg_p
231                 return
232         }
233
234         ui_status [mc "Calling pre-commit hook..."]
235         set pch_error {}
236         fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
237         fileevent $fd_ph readable \
238                 [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
239 }
240
241 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
242         global pch_error
243
244         append pch_error [read $fd_ph]
245         fconfigure $fd_ph -blocking 1
246         if {[eof $fd_ph]} {
247                 if {[catch {close $fd_ph}]} {
248                         catch {file delete $msg_p}
249                         ui_status [mc "Commit declined by pre-commit hook."]
250                         hook_failed_popup pre-commit $pch_error
251                         unlock_index
252                 } else {
253                         commit_commitmsg $curHEAD $msg_p
254                 }
255                 set pch_error {}
256                 return
257         }
258         fconfigure $fd_ph -blocking 0
259 }
260
261 proc commit_commitmsg {curHEAD msg_p} {
262         global pch_error
263
264         # -- Run the commit-msg hook.
265         #
266         set fd_ph [githook_read commit-msg $msg_p]
267         if {$fd_ph eq {}} {
268                 commit_writetree $curHEAD $msg_p
269                 return
270         }
271
272         ui_status [mc "Calling commit-msg hook..."]
273         set pch_error {}
274         fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
275         fileevent $fd_ph readable \
276                 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
277 }
278
279 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
280         global pch_error
281
282         append pch_error [read $fd_ph]
283         fconfigure $fd_ph -blocking 1
284         if {[eof $fd_ph]} {
285                 if {[catch {close $fd_ph}]} {
286                         catch {file delete $msg_p}
287                         ui_status [mc "Commit declined by commit-msg hook."]
288                         hook_failed_popup commit-msg $pch_error
289                         unlock_index
290                 } else {
291                         commit_writetree $curHEAD $msg_p
292                 }
293                 set pch_error {}
294                 return
295         }
296         fconfigure $fd_ph -blocking 0
297 }
298
299 proc commit_writetree {curHEAD msg_p} {
300         ui_status [mc "Committing changes..."]
301         set fd_wt [git_read write-tree]
302         fileevent $fd_wt readable \
303                 [list commit_committree $fd_wt $curHEAD $msg_p]
304 }
305
306 proc commit_committree {fd_wt curHEAD msg_p} {
307         global HEAD PARENT MERGE_HEAD commit_type
308         global current_branch
309         global ui_comm selected_commit_type
310         global file_states selected_paths rescan_active
311         global repo_config
312
313         gets $fd_wt tree_id
314         if {[catch {close $fd_wt} err]} {
315                 catch {file delete $msg_p}
316                 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
317                 ui_status [mc "Commit failed."]
318                 unlock_index
319                 return
320         }
321
322         # -- Verify this wasn't an empty change.
323         #
324         if {$commit_type eq {normal}} {
325                 set fd_ot [git_read cat-file commit $PARENT]
326                 fconfigure $fd_ot -encoding binary -translation lf
327                 set old_tree [gets $fd_ot]
328                 close $fd_ot
329
330                 if {[string equal -length 5 {tree } $old_tree]
331                         && [string length $old_tree] == 45} {
332                         set old_tree [string range $old_tree 5 end]
333                 } else {
334                         error [mc "Commit %s appears to be corrupt" $PARENT]
335                 }
336
337                 if {$tree_id eq $old_tree} {
338                         catch {file delete $msg_p}
339                         info_popup [mc "No changes to commit.
340
341 No files were modified by this commit and it was not a merge commit.
342
343 A rescan will be automatically started now.
344 "]
345                         unlock_index
346                         rescan {ui_status [mc "No changes to commit."]}
347                         return
348                 }
349         }
350
351         # -- Create the commit.
352         #
353         set cmd [list commit-tree $tree_id]
354         foreach p [concat $PARENT $MERGE_HEAD] {
355                 lappend cmd -p $p
356         }
357         lappend cmd <$msg_p
358         if {[catch {set cmt_id [eval git $cmd]} err]} {
359                 catch {file delete $msg_p}
360                 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
361                 ui_status [mc "Commit failed."]
362                 unlock_index
363                 return
364         }
365
366         # -- Update the HEAD ref.
367         #
368         set reflogm commit
369         if {$commit_type ne {normal}} {
370                 append reflogm " ($commit_type)"
371         }
372         set msg_fd [open $msg_p r]
373         setup_commit_encoding $msg_fd 1
374         gets $msg_fd subject
375         close $msg_fd
376         append reflogm {: } $subject
377         if {[catch {
378                         git update-ref -m $reflogm HEAD $cmt_id $curHEAD
379                 } err]} {
380                 catch {file delete $msg_p}
381                 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
382                 ui_status [mc "Commit failed."]
383                 unlock_index
384                 return
385         }
386
387         # -- Cleanup after ourselves.
388         #
389         catch {file delete $msg_p}
390         catch {file delete [gitdir MERGE_HEAD]}
391         catch {file delete [gitdir MERGE_MSG]}
392         catch {file delete [gitdir SQUASH_MSG]}
393         catch {file delete [gitdir GITGUI_MSG]}
394
395         # -- Let rerere do its thing.
396         #
397         if {[get_config rerere.enabled] eq {}} {
398                 set rerere [file isdirectory [gitdir rr-cache]]
399         } else {
400                 set rerere [is_config_true rerere.enabled]
401         }
402         if {$rerere} {
403                 catch {git rerere}
404         }
405
406         # -- Run the post-commit hook.
407         #
408         set fd_ph [githook_read post-commit]
409         if {$fd_ph ne {}} {
410                 global pch_error
411                 set pch_error {}
412                 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
413                 fileevent $fd_ph readable \
414                         [list commit_postcommit_wait $fd_ph $cmt_id]
415         }
416
417         $ui_comm delete 0.0 end
418         $ui_comm edit reset
419         $ui_comm edit modified false
420         if {$::GITGUI_BCK_exists} {
421                 catch {file delete [gitdir GITGUI_BCK]}
422                 set ::GITGUI_BCK_exists 0
423         }
424
425         if {[is_enabled singlecommit]} { do_quit 0 }
426
427         # -- Update in memory status
428         #
429         set selected_commit_type new
430         set commit_type normal
431         set HEAD $cmt_id
432         set PARENT $cmt_id
433         set MERGE_HEAD [list]
434
435         foreach path [array names file_states] {
436                 set s $file_states($path)
437                 set m [lindex $s 0]
438                 switch -glob -- $m {
439                 _O -
440                 _M -
441                 _D {continue}
442                 __ -
443                 A_ -
444                 M_ -
445                 T_ -
446                 D_ {
447                         unset file_states($path)
448                         catch {unset selected_paths($path)}
449                 }
450                 DO {
451                         set file_states($path) [list _O [lindex $s 1] {} {}]
452                 }
453                 AM -
454                 AD -
455                 MM -
456                 MD {
457                         set file_states($path) [list \
458                                 _[string index $m 1] \
459                                 [lindex $s 1] \
460                                 [lindex $s 3] \
461                                 {}]
462                 }
463                 }
464         }
465
466         display_all_files
467         unlock_index
468         reshow_diff
469         ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
470 }
471
472 proc commit_postcommit_wait {fd_ph cmt_id} {
473         global pch_error
474
475         append pch_error [read $fd_ph]
476         fconfigure $fd_ph -blocking 1
477         if {[eof $fd_ph]} {
478                 if {[catch {close $fd_ph}]} {
479                         hook_failed_popup post-commit $pch_error 0
480                 }
481                 unset pch_error
482                 return
483         }
484         fconfigure $fd_ph -blocking 0
485 }