Code

Merge branch 'jc/maint-log-first-parent-pathspec' into maint
[git.git] / git-gui / lib / commit.tcl
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc load_last_commit {} {
5         global HEAD PARENT MERGE_HEAD commit_type ui_comm
6         global repo_config
8         if {[llength $PARENT] == 0} {
9                 error_popup [mc "There is nothing to amend.
11 You are about to create the initial commit.  There is no commit before this to amend.
12 "]
13                 return
14         }
16         repository_state curType curHEAD curMERGE_HEAD
17         if {$curType eq {merge}} {
18                 error_popup [mc "Cannot amend while merging.
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         }
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
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         }
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         }
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 }
68 set GIT_COMMITTER_IDENT {}
70 proc committer_ident {} {
71         global GIT_COMMITTER_IDENT
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         }
85         return $GIT_COMMITTER_IDENT
86 }
88 proc do_signoff {} {
89         global ui_comm
91         set me [committer_ident]
92         if {$me eq {}} return
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         }
108 proc create_new_commit {} {
109         global commit_type ui_comm
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
118 proc setup_commit_encoding {msg_wt {quiet 0}} {
119         global repo_config
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         }
135 proc commit_tree {} {
136         global HEAD commit_type file_states ui_comm repo_config
137         global pch_error
139         if {[committer_ident] eq {}} return
140         if {![lock_index update]} return
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.
151 Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
153 The rescan will be automatically started now.
154 "]
155                 unlock_index
156                 rescan ui_ready
157                 return
158         }
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.
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.
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.
191 You must stage at least 1 file before you can commit.
192 "]
193                 unlock_index
194                 return
195         }
197         if {[is_enabled nocommitmsg]} { do_quit 0 }
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.
206 A good commit message has the following format:
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         }
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
225         if {[is_enabled nocommit]} { do_quit 0 }
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         }
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]
242 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
243         global pch_error
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
262 proc commit_commitmsg {curHEAD msg_p} {
263         global is_detached repo_config
264         global pch_error
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 loose 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         }
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         }
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]
297 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
298         global pch_error
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
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]
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
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         }
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
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                 }
355                 if {$tree_id eq $old_tree} {
356                         catch {file delete $msg_p}
357                         info_popup [mc "No changes to commit.
359 No files were modified by this commit and it was not a merge commit.
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         }
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         }
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         }
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]}
413         # -- Let rerere do its thing.
414         #
415         if {[get_config rerere.enabled] eq {}} {
416                 set rerere [file isdirectory [gitdir rr-cache]]
417         } else {
418                 set rerere [is_config_true rerere.enabled]
419         }
420         if {$rerere} {
421                 catch {git rerere}
422         }
424         # -- Run the post-commit hook.
425         #
426         set fd_ph [githook_read post-commit]
427         if {$fd_ph ne {}} {
428                 global pch_error
429                 set pch_error {}
430                 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
431                 fileevent $fd_ph readable \
432                         [list commit_postcommit_wait $fd_ph $cmt_id]
433         }
435         $ui_comm delete 0.0 end
436         $ui_comm edit reset
437         $ui_comm edit modified false
438         if {$::GITGUI_BCK_exists} {
439                 catch {file delete [gitdir GITGUI_BCK]}
440                 set ::GITGUI_BCK_exists 0
441         }
443         if {[is_enabled singlecommit]} { do_quit 0 }
445         # -- Update in memory status
446         #
447         set selected_commit_type new
448         set commit_type normal
449         set HEAD $cmt_id
450         set PARENT $cmt_id
451         set MERGE_HEAD [list]
453         foreach path [array names file_states] {
454                 set s $file_states($path)
455                 set m [lindex $s 0]
456                 switch -glob -- $m {
457                 _O -
458                 _M -
459                 _D {continue}
460                 __ -
461                 A_ -
462                 M_ -
463                 T_ -
464                 D_ {
465                         unset file_states($path)
466                         catch {unset selected_paths($path)}
467                 }
468                 DO {
469                         set file_states($path) [list _O [lindex $s 1] {} {}]
470                 }
471                 AM -
472                 AD -
473                 AT -
474                 TM -
475                 TD -
476                 MM -
477                 MT -
478                 MD {
479                         set file_states($path) [list \
480                                 _[string index $m 1] \
481                                 [lindex $s 1] \
482                                 [lindex $s 3] \
483                                 {}]
484                 }
485                 }
486         }
488         display_all_files
489         unlock_index
490         reshow_diff
491         ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
494 proc commit_postcommit_wait {fd_ph cmt_id} {
495         global pch_error
497         append pch_error [read $fd_ph]
498         fconfigure $fd_ph -blocking 1
499         if {[eof $fd_ph]} {
500                 if {[catch {close $fd_ph}]} {
501                         hook_failed_popup post-commit $pch_error 0
502                 }
503                 unset pch_error
504                 return
505         }
506         fconfigure $fd_ph -blocking 0