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