Merge branch 'tg/add-chmod+x-fix'
[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                 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
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
179                 }
180                 default {
181                         error_popup [mc "Unknown file state %s detected.
182
183 File %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
191 You 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
206 A 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
242 proc 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
262 proc 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.\
270 This is a potentially dangerous thing to do because if you switch\
271 to another branch you will lose your changes and it can be difficult\
272 to retrieve them later from the reflog. You should probably cancel this\
273 commit and create a new branch to continue.\n\
274 \n\
275 Do 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
297 proc 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
317 proc 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
324 proc 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
359 No files were modified by this commit and it was not a merge commit.
360
361 A 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
498 proc 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 }