Merge branch 'tm/completion-push-set-upstream'
[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 && $repo_config(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\
272 \n\
273 Do you really want to proceed with your Commit?"]
274                 if {[ask_popup $msg] ne yes} {
275                         unlock_index
276                         return
277                 }
278         }
279
280         # -- Run the commit-msg hook.
281         #
282         set fd_ph [githook_read commit-msg $msg_p]
283         if {$fd_ph eq {}} {
284                 commit_writetree $curHEAD $msg_p
285                 return
286         }
287
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]
293 }
294
295 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
296         global pch_error
297
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
308                 }
309                 set pch_error {}
310                 return
311         }
312         fconfigure $fd_ph -blocking 0
313 }
314
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]
320 }
321
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
328
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
336         }
337
338         # -- Verify this wasn't an empty change.
339         #
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
345
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]
351                 }
352
353                 if {$tree_id eq $old_tree} {
354                         catch {file delete $msg_p}
355                         info_popup [mc "No changes to commit.
356
357 No files were modified by this commit and it was not a merge commit.
358
359 A rescan will be automatically started now.
360 "]
361                         unlock_index
362                         rescan {ui_status [mc "No changes to commit."]}
363                         return
364                 }
365         }
366
367         # -- Create the commit.
368         #
369         set cmd [list commit-tree $tree_id]
370         foreach p [concat $PARENT $MERGE_HEAD] {
371                 lappend cmd -p $p
372         }
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
380         }
381
382         # -- Update the HEAD ref.
383         #
384         set reflogm commit
385         if {$commit_type ne {normal}} {
386                 append reflogm " ($commit_type)"
387         }
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
401         }
402
403         # -- Cleanup after ourselves.
404         #
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]}
410
411         # -- Let rerere do its thing.
412         #
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]
417         }
418         if {$rerere} {
419                 catch {git rerere}
420         }
421
422         # -- Run the post-commit hook.
423         #
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]
431         }
432
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
439         }
440
441         if {[is_enabled singlecommit]} { do_quit 0 }
442
443         # -- Update in memory status
444         #
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]
450
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)}
465                 }
466                 DO {
467                         set file_states($path) [list _O [lindex $s 1] {} {}]
468                 }
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] \
481                                 {}]
482                 }
483                 }
484         }
485
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]
490 }
491
492 proc commit_postcommit_wait {fd_ph cmt_id} {
493         global pch_error
494
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
500                 }
501                 unset pch_error
502                 return
503         }
504         fconfigure $fd_ph -blocking 0
505 }