Merge branch 'jk/tag-contains-ab'
[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 pch_error
264
265         # -- Run the commit-msg hook.
266         #
267         set fd_ph [githook_read commit-msg $msg_p]
268         if {$fd_ph eq {}} {
269                 commit_writetree $curHEAD $msg_p
270                 return
271         }
272
273         ui_status [mc "Calling commit-msg hook..."]
274         set pch_error {}
275         fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
276         fileevent $fd_ph readable \
277                 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
278 }
279
280 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
281         global pch_error
282
283         append pch_error [read $fd_ph]
284         fconfigure $fd_ph -blocking 1
285         if {[eof $fd_ph]} {
286                 if {[catch {close $fd_ph}]} {
287                         catch {file delete $msg_p}
288                         ui_status [mc "Commit declined by commit-msg hook."]
289                         hook_failed_popup commit-msg $pch_error
290                         unlock_index
291                 } else {
292                         commit_writetree $curHEAD $msg_p
293                 }
294                 set pch_error {}
295                 return
296         }
297         fconfigure $fd_ph -blocking 0
298 }
299
300 proc commit_writetree {curHEAD msg_p} {
301         ui_status [mc "Committing changes..."]
302         set fd_wt [git_read write-tree]
303         fileevent $fd_wt readable \
304                 [list commit_committree $fd_wt $curHEAD $msg_p]
305 }
306
307 proc commit_committree {fd_wt curHEAD msg_p} {
308         global HEAD PARENT MERGE_HEAD commit_type
309         global current_branch
310         global ui_comm selected_commit_type
311         global file_states selected_paths rescan_active
312         global repo_config
313
314         gets $fd_wt tree_id
315         if {[catch {close $fd_wt} err]} {
316                 catch {file delete $msg_p}
317                 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
318                 ui_status [mc "Commit failed."]
319                 unlock_index
320                 return
321         }
322
323         # -- Verify this wasn't an empty change.
324         #
325         if {$commit_type eq {normal}} {
326                 set fd_ot [git_read cat-file commit $PARENT]
327                 fconfigure $fd_ot -encoding binary -translation lf
328                 set old_tree [gets $fd_ot]
329                 close $fd_ot
330
331                 if {[string equal -length 5 {tree } $old_tree]
332                         && [string length $old_tree] == 45} {
333                         set old_tree [string range $old_tree 5 end]
334                 } else {
335                         error [mc "Commit %s appears to be corrupt" $PARENT]
336                 }
337
338                 if {$tree_id eq $old_tree} {
339                         catch {file delete $msg_p}
340                         info_popup [mc "No changes to commit.
341
342 No files were modified by this commit and it was not a merge commit.
343
344 A rescan will be automatically started now.
345 "]
346                         unlock_index
347                         rescan {ui_status [mc "No changes to commit."]}
348                         return
349                 }
350         }
351
352         # -- Create the commit.
353         #
354         set cmd [list commit-tree $tree_id]
355         foreach p [concat $PARENT $MERGE_HEAD] {
356                 lappend cmd -p $p
357         }
358         lappend cmd <$msg_p
359         if {[catch {set cmt_id [eval git $cmd]} err]} {
360                 catch {file delete $msg_p}
361                 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
362                 ui_status [mc "Commit failed."]
363                 unlock_index
364                 return
365         }
366
367         # -- Update the HEAD ref.
368         #
369         set reflogm commit
370         if {$commit_type ne {normal}} {
371                 append reflogm " ($commit_type)"
372         }
373         set msg_fd [open $msg_p r]
374         setup_commit_encoding $msg_fd 1
375         gets $msg_fd subject
376         close $msg_fd
377         append reflogm {: } $subject
378         if {[catch {
379                         git update-ref -m $reflogm HEAD $cmt_id $curHEAD
380                 } err]} {
381                 catch {file delete $msg_p}
382                 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
383                 ui_status [mc "Commit failed."]
384                 unlock_index
385                 return
386         }
387
388         # -- Cleanup after ourselves.
389         #
390         catch {file delete $msg_p}
391         catch {file delete [gitdir MERGE_HEAD]}
392         catch {file delete [gitdir MERGE_MSG]}
393         catch {file delete [gitdir SQUASH_MSG]}
394         catch {file delete [gitdir GITGUI_MSG]}
395
396         # -- Let rerere do its thing.
397         #
398         if {[get_config rerere.enabled] eq {}} {
399                 set rerere [file isdirectory [gitdir rr-cache]]
400         } else {
401                 set rerere [is_config_true rerere.enabled]
402         }
403         if {$rerere} {
404                 catch {git rerere}
405         }
406
407         # -- Run the post-commit hook.
408         #
409         set fd_ph [githook_read post-commit]
410         if {$fd_ph ne {}} {
411                 global pch_error
412                 set pch_error {}
413                 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
414                 fileevent $fd_ph readable \
415                         [list commit_postcommit_wait $fd_ph $cmt_id]
416         }
417
418         $ui_comm delete 0.0 end
419         $ui_comm edit reset
420         $ui_comm edit modified false
421         if {$::GITGUI_BCK_exists} {
422                 catch {file delete [gitdir GITGUI_BCK]}
423                 set ::GITGUI_BCK_exists 0
424         }
425
426         if {[is_enabled singlecommit]} { do_quit 0 }
427
428         # -- Update in memory status
429         #
430         set selected_commit_type new
431         set commit_type normal
432         set HEAD $cmt_id
433         set PARENT $cmt_id
434         set MERGE_HEAD [list]
435
436         foreach path [array names file_states] {
437                 set s $file_states($path)
438                 set m [lindex $s 0]
439                 switch -glob -- $m {
440                 _O -
441                 _M -
442                 _D {continue}
443                 __ -
444                 A_ -
445                 M_ -
446                 T_ -
447                 D_ {
448                         unset file_states($path)
449                         catch {unset selected_paths($path)}
450                 }
451                 DO {
452                         set file_states($path) [list _O [lindex $s 1] {} {}]
453                 }
454                 AM -
455                 AD -
456                 AT -
457                 TM -
458                 TD -
459                 MM -
460                 MT -
461                 MD {
462                         set file_states($path) [list \
463                                 _[string index $m 1] \
464                                 [lindex $s 1] \
465                                 [lindex $s 3] \
466                                 {}]
467                 }
468                 }
469         }
470
471         display_all_files
472         unlock_index
473         reshow_diff
474         ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
475 }
476
477 proc commit_postcommit_wait {fd_ph cmt_id} {
478         global pch_error
479
480         append pch_error [read $fd_ph]
481         fconfigure $fd_ph -blocking 1
482         if {[eof $fd_ph]} {
483                 if {[catch {close $fd_ph}]} {
484                         hook_failed_popup post-commit $pch_error 0
485                 }
486                 unset pch_error
487                 return
488         }
489         fconfigure $fd_ph -blocking 0
490 }