| 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 [list cat-file commit $curHEAD]] |
| 31 | fconfigure $fd -encoding iso8859-1 |
| 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 [convertfrom $enc $msg] |
| 47 | set name [convertfrom $enc $name] |
| 48 | set email [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 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 | |
| 160 | Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created. |
| 161 | |
| 162 | The 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 | |
| 184 | File %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 | |
| 192 | File %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 | |
| 200 | You 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 [$ui_comm get 1.0 end] |
| 211 | |
| 212 | # -- Build the message file. |
| 213 | # |
| 214 | set msg_p [gitdir GITGUI_EDITMSG] |
| 215 | set msg_wt [safe_open_file $msg_p w] |
| 216 | fconfigure $msg_wt -translation lf |
| 217 | setup_commit_encoding $msg_wt |
| 218 | puts $msg_wt $msg |
| 219 | close $msg_wt |
| 220 | |
| 221 | if {[is_enabled nocommit]} { do_quit 0 } |
| 222 | |
| 223 | # -- Run the pre-commit hook. |
| 224 | # |
| 225 | set fd_ph [githook_read pre-commit] |
| 226 | if {$fd_ph eq {}} { |
| 227 | commit_commitmsg $curHEAD $msg_p |
| 228 | return |
| 229 | } |
| 230 | |
| 231 | ui_status [mc "Calling pre-commit hook..."] |
| 232 | set pch_error {} |
| 233 | fconfigure $fd_ph -blocking 0 -translation binary |
| 234 | fileevent $fd_ph readable \ |
| 235 | [list commit_prehook_wait $fd_ph $curHEAD $msg_p] |
| 236 | } |
| 237 | |
| 238 | proc commit_prehook_wait {fd_ph curHEAD msg_p} { |
| 239 | global pch_error |
| 240 | |
| 241 | append pch_error [read $fd_ph] |
| 242 | fconfigure $fd_ph -blocking 1 |
| 243 | if {[eof $fd_ph]} { |
| 244 | if {[catch {close $fd_ph}]} { |
| 245 | catch {file delete $msg_p} |
| 246 | ui_status [mc "Commit declined by pre-commit hook."] |
| 247 | hook_failed_popup pre-commit $pch_error |
| 248 | unlock_index |
| 249 | } else { |
| 250 | commit_commitmsg $curHEAD $msg_p |
| 251 | } |
| 252 | set pch_error {} |
| 253 | return |
| 254 | } |
| 255 | fconfigure $fd_ph -blocking 0 |
| 256 | } |
| 257 | |
| 258 | proc commit_commitmsg {curHEAD msg_p} { |
| 259 | global is_detached repo_config |
| 260 | global pch_error |
| 261 | |
| 262 | if {$is_detached |
| 263 | && ![file exists [gitdir rebase-merge head-name]] |
| 264 | && [is_config_true gui.warndetachedcommit]} { |
| 265 | set msg [mc "You are about to commit on a detached head.\ |
| 266 | This is a potentially dangerous thing to do because if you switch\ |
| 267 | to another branch you will lose your changes and it can be difficult\ |
| 268 | to retrieve them later from the reflog. You should probably cancel this\ |
| 269 | commit and create a new branch to continue.\n\ |
| 270 | \n\ |
| 271 | Do you really want to proceed with your Commit?"] |
| 272 | if {[ask_popup $msg] ne yes} { |
| 273 | unlock_index |
| 274 | return |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | # -- Run the commit-msg hook. |
| 279 | # |
| 280 | set fd_ph [githook_read commit-msg $msg_p] |
| 281 | if {$fd_ph eq {}} { |
| 282 | commit_writetree $curHEAD $msg_p |
| 283 | return |
| 284 | } |
| 285 | |
| 286 | ui_status [mc "Calling commit-msg hook..."] |
| 287 | set pch_error {} |
| 288 | fconfigure $fd_ph -blocking 0 -translation binary |
| 289 | fileevent $fd_ph readable \ |
| 290 | [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p] |
| 291 | } |
| 292 | |
| 293 | proc commit_commitmsg_wait {fd_ph curHEAD msg_p} { |
| 294 | global pch_error |
| 295 | |
| 296 | append pch_error [read $fd_ph] |
| 297 | fconfigure $fd_ph -blocking 1 |
| 298 | if {[eof $fd_ph]} { |
| 299 | if {[catch {close $fd_ph}]} { |
| 300 | catch {file delete $msg_p} |
| 301 | ui_status [mc "Commit declined by commit-msg hook."] |
| 302 | hook_failed_popup commit-msg $pch_error |
| 303 | unlock_index |
| 304 | } else { |
| 305 | commit_writetree $curHEAD $msg_p |
| 306 | } |
| 307 | set pch_error {} |
| 308 | return |
| 309 | } |
| 310 | fconfigure $fd_ph -blocking 0 |
| 311 | } |
| 312 | |
| 313 | proc wash_commit_message {msg} { |
| 314 | # Strip trailing whitespace |
| 315 | regsub -all -line {[ \t\r]+$} $msg {} msg |
| 316 | # Strip comment lines |
| 317 | global comment_string |
| 318 | set cmt_rx [strcat {(^|\n)} [regsub -all {\W} $comment_string {\\&}] {[^\n]*}] |
| 319 | regsub -all $cmt_rx $msg {\1} msg |
| 320 | # Strip leading and trailing empty lines (puts adds one \n) |
| 321 | set msg [string trim $msg \n] |
| 322 | # Compress consecutive empty lines |
| 323 | regsub -all {\n{3,}} $msg \n\n msg |
| 324 | |
| 325 | return $msg |
| 326 | } |
| 327 | |
| 328 | proc commit_writetree {curHEAD msg_p} { |
| 329 | # -- Process the commit message after hooks have run. |
| 330 | # |
| 331 | set msg_fd [safe_open_file $msg_p r] |
| 332 | setup_commit_encoding $msg_fd 1 |
| 333 | set msg [read $msg_fd] |
| 334 | close $msg_fd |
| 335 | |
| 336 | # Process the message (strip whitespace, comments, etc.) |
| 337 | set msg [wash_commit_message $msg] |
| 338 | |
| 339 | if {$msg eq {}} { |
| 340 | error_popup [mc "Please supply a commit message. |
| 341 | |
| 342 | A good commit message has the following format: |
| 343 | |
| 344 | - First line: Describe in one sentence what you did. |
| 345 | - Second line: Blank |
| 346 | - Remaining lines: Describe why this change is good. |
| 347 | "] |
| 348 | unlock_index |
| 349 | return |
| 350 | } |
| 351 | |
| 352 | # Write the processed message back to the file |
| 353 | set msg_wt [safe_open_file $msg_p w] |
| 354 | fconfigure $msg_wt -translation lf |
| 355 | setup_commit_encoding $msg_wt |
| 356 | puts $msg_wt $msg |
| 357 | close $msg_wt |
| 358 | |
| 359 | ui_status [mc "Committing changes..."] |
| 360 | set fd_wt [git_read [list write-tree]] |
| 361 | fileevent $fd_wt readable \ |
| 362 | [list commit_committree $fd_wt $curHEAD $msg_p] |
| 363 | } |
| 364 | |
| 365 | proc commit_committree {fd_wt curHEAD msg_p} { |
| 366 | global HEAD PARENT MERGE_HEAD commit_type commit_author |
| 367 | global current_branch |
| 368 | global ui_comm commit_type_is_amend |
| 369 | global file_states selected_paths rescan_active |
| 370 | global repo_config |
| 371 | global env |
| 372 | global hashlength |
| 373 | |
| 374 | gets $fd_wt tree_id |
| 375 | if {[catch {close $fd_wt} err]} { |
| 376 | catch {file delete $msg_p} |
| 377 | error_popup [strcat [mc "write-tree failed:"] "\n\n$err"] |
| 378 | ui_status [mc "Commit failed."] |
| 379 | unlock_index |
| 380 | return |
| 381 | } |
| 382 | |
| 383 | # -- Verify this wasn't an empty change. |
| 384 | # |
| 385 | if {$commit_type eq {normal}} { |
| 386 | set fd_ot [git_read [list cat-file commit $PARENT]] |
| 387 | fconfigure $fd_ot -encoding iso8859-1 |
| 388 | set old_tree [gets $fd_ot] |
| 389 | close $fd_ot |
| 390 | |
| 391 | if {[string equal -length 5 {tree } $old_tree] |
| 392 | && [string length $old_tree] == [expr {$hashlength + 5}]} { |
| 393 | set old_tree [string range $old_tree 5 end] |
| 394 | } else { |
| 395 | error [mc "Commit %s appears to be corrupt" $PARENT] |
| 396 | } |
| 397 | |
| 398 | if {$tree_id eq $old_tree} { |
| 399 | catch {file delete $msg_p} |
| 400 | info_popup [mc "No changes to commit. |
| 401 | |
| 402 | No files were modified by this commit and it was not a merge commit. |
| 403 | |
| 404 | A rescan will be automatically started now. |
| 405 | "] |
| 406 | unlock_index |
| 407 | rescan {ui_status [mc "No changes to commit."]} |
| 408 | return |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | if {[info exists commit_author]} { |
| 413 | set old_author [commit_author_ident $commit_author] |
| 414 | } |
| 415 | # -- Create the commit. |
| 416 | # |
| 417 | set cmd [list commit-tree $tree_id] |
| 418 | if {[is_config_true commit.gpgsign]} { |
| 419 | lappend cmd -S |
| 420 | } |
| 421 | foreach p [concat $PARENT $MERGE_HEAD] { |
| 422 | lappend cmd -p $p |
| 423 | } |
| 424 | set msgtxt [list <$msg_p] |
| 425 | if {[catch {set cmt_id [git_redir $cmd $msgtxt]} err]} { |
| 426 | catch {file delete $msg_p} |
| 427 | error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"] |
| 428 | ui_status [mc "Commit failed."] |
| 429 | unlock_index |
| 430 | unset -nocomplain commit_author |
| 431 | commit_author_reset $old_author |
| 432 | return |
| 433 | } |
| 434 | if {[info exists commit_author]} { |
| 435 | unset -nocomplain commit_author |
| 436 | commit_author_reset $old_author |
| 437 | } |
| 438 | |
| 439 | # -- Update the HEAD ref. |
| 440 | # |
| 441 | set reflogm commit |
| 442 | if {$commit_type ne {normal}} { |
| 443 | append reflogm " ($commit_type)" |
| 444 | } |
| 445 | set msg_fd [safe_open_file $msg_p r] |
| 446 | setup_commit_encoding $msg_fd 1 |
| 447 | gets $msg_fd subject |
| 448 | close $msg_fd |
| 449 | append reflogm {: } $subject |
| 450 | if {[catch { |
| 451 | git update-ref -m $reflogm HEAD $cmt_id $curHEAD |
| 452 | } err]} { |
| 453 | catch {file delete $msg_p} |
| 454 | error_popup [strcat [mc "update-ref failed:"] "\n\n$err"] |
| 455 | ui_status [mc "Commit failed."] |
| 456 | unlock_index |
| 457 | return |
| 458 | } |
| 459 | |
| 460 | # -- Cleanup after ourselves. |
| 461 | # |
| 462 | catch {file delete $msg_p} |
| 463 | catch {file delete [gitdir MERGE_HEAD]} |
| 464 | catch {file delete [gitdir MERGE_MSG]} |
| 465 | catch {file delete [gitdir SQUASH_MSG]} |
| 466 | catch {file delete [gitdir GITGUI_MSG]} |
| 467 | catch {file delete [gitdir CHERRY_PICK_HEAD]} |
| 468 | |
| 469 | # -- Let rerere do its thing. |
| 470 | # |
| 471 | if {[get_config rerere.enabled] eq {}} { |
| 472 | set rerere [file isdirectory [gitdir rr-cache]] |
| 473 | } else { |
| 474 | set rerere [is_config_true rerere.enabled] |
| 475 | } |
| 476 | if {$rerere} { |
| 477 | catch {git rerere} |
| 478 | } |
| 479 | |
| 480 | # -- Run the post-commit hook. |
| 481 | # |
| 482 | set fd_ph [githook_read post-commit] |
| 483 | if {$fd_ph ne {}} { |
| 484 | global pch_error |
| 485 | set pch_error {} |
| 486 | fconfigure $fd_ph -blocking 0 -translation binary |
| 487 | fileevent $fd_ph readable \ |
| 488 | [list commit_postcommit_wait $fd_ph $cmt_id] |
| 489 | } |
| 490 | |
| 491 | $ui_comm delete 0.0 end |
| 492 | load_message [get_config commit.template] |
| 493 | $ui_comm edit reset |
| 494 | $ui_comm edit modified false |
| 495 | if {$::GITGUI_BCK_exists} { |
| 496 | catch {file delete [gitdir GITGUI_BCK]} |
| 497 | set ::GITGUI_BCK_exists 0 |
| 498 | } |
| 499 | |
| 500 | if {[is_enabled singlecommit]} { do_quit 0 } |
| 501 | |
| 502 | # -- Update in memory status |
| 503 | # |
| 504 | set commit_type normal |
| 505 | set commit_type_is_amend 0 |
| 506 | set HEAD $cmt_id |
| 507 | set PARENT $cmt_id |
| 508 | set MERGE_HEAD [list] |
| 509 | |
| 510 | foreach path [array names file_states] { |
| 511 | set s $file_states($path) |
| 512 | set m [lindex $s 0] |
| 513 | switch -glob -- $m { |
| 514 | _O - |
| 515 | _M - |
| 516 | _D {continue} |
| 517 | __ - |
| 518 | A_ - |
| 519 | M_ - |
| 520 | T_ - |
| 521 | D_ { |
| 522 | unset file_states($path) |
| 523 | catch {unset selected_paths($path)} |
| 524 | } |
| 525 | DO { |
| 526 | set file_states($path) [list _O [lindex $s 1] {} {}] |
| 527 | } |
| 528 | AM - |
| 529 | AD - |
| 530 | AT - |
| 531 | TM - |
| 532 | TD - |
| 533 | MM - |
| 534 | MT - |
| 535 | MD { |
| 536 | set file_states($path) [list \ |
| 537 | _[string index $m 1] \ |
| 538 | [lindex $s 1] \ |
| 539 | [lindex $s 3] \ |
| 540 | {}] |
| 541 | } |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | display_all_files |
| 546 | unlock_index |
| 547 | reshow_diff |
| 548 | ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject] |
| 549 | } |
| 550 | |
| 551 | proc commit_postcommit_wait {fd_ph cmt_id} { |
| 552 | global pch_error |
| 553 | |
| 554 | append pch_error [read $fd_ph] |
| 555 | fconfigure $fd_ph -blocking 1 |
| 556 | if {[eof $fd_ph]} { |
| 557 | if {[catch {close $fd_ph}]} { |
| 558 | hook_failed_popup post-commit $pch_error 0 |
| 559 | } |
| 560 | unset pch_error |
| 561 | return |
| 562 | } |
| 563 | fconfigure $fd_ph -blocking 0 |
| 564 | } |
| 565 | |
| 566 | proc commit_author_ident {details} { |
| 567 | global env |
| 568 | array set author $details |
| 569 | set old [array get env GIT_AUTHOR_*] |
| 570 | set env(GIT_AUTHOR_NAME) $author(name) |
| 571 | set env(GIT_AUTHOR_EMAIL) $author(email) |
| 572 | set env(GIT_AUTHOR_DATE) $author(date) |
| 573 | return $old |
| 574 | } |
| 575 | proc commit_author_reset {details} { |
| 576 | global env |
| 577 | unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE) |
| 578 | if {$details ne {}} { |
| 579 | array set env $details |
| 580 | } |
| 581 | } |