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