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