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