git-gui: Do not reset author details on amend
[git] / lib / commit.tcl
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 set author_name ""
5 set author_email ""
6 set author_date ""
7
8 proc 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
16 You 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
25 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.
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
77 set GIT_COMMITTER_IDENT {}
78
79 proc 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
97 proc 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
117 proc 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
131 proc 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
148 proc 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
164 Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
165
166 The 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
188 File %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
196 File %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
204 You 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
219 A 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
255 proc 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
275 proc 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.\
283 This is a potentially dangerous thing to do because if you switch\
284 to another branch you will lose your changes and it can be difficult\
285 to retrieve them later from the reflog. You should probably cancel this\
286 commit and create a new branch to continue.\n\
287 \n\
288 Do 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
310 proc 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
330 proc 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
337 proc 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
373 No files were modified by this commit and it was not a merge commit.
374
375 A 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
514 proc 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 }