1# git-gui commit checkout support 2# Copyright (C) 2007 Shawn Pearce 3 4class checkout_op { 5 6field w {}; # our window (if we have one) 7field w_cons {}; # embedded console window object 8 9field new_expr ; # expression the user saw/thinks this is 10field new_hash ; # commit SHA-1 we are switching to 11field new_ref ; # ref we are updating/creating 12field old_hash ; # commit SHA-1 that was checked out when we started 13 14field parent_w .; # window that started us 15field merge_type none; # type of merge to apply to existing branch 16field merge_base {}; # merge base if we have another ref involved 17field fetch_spec {}; # refetch tracking branch if used? 18field checkout 1; # actually checkout the branch? 19field create 0; # create the branch if it doesn't exist? 20field remote_source {}; # same as fetch_spec, to setup tracking 21 22field reset_ok 0; # did the user agree to reset? 23field fetch_ok 0; # did the fetch succeed? 24 25field readtree_d {}; # buffered output from read-tree 26field update_old {}; # was the update-ref call deferred? 27field reflog_msg {}; # log message for the update-ref call 28 29constructor new {expr hash {ref {}}} { 30 set new_expr $expr 31 set new_hash $hash 32 set new_ref $ref 33 34 return $this 35} 36 37method parent {path} { 38 set parent_w [winfo toplevel $path] 39} 40 41method enable_merge {type} { 42 set merge_type $type 43} 44 45method enable_fetch {spec} { 46 set fetch_spec $spec 47} 48 49method remote_source {spec} { 50 set remote_source $spec 51} 52 53method enable_checkout {co} { 54 set checkout $co 55} 56 57method enable_create {co} { 58 set create $co 59} 60 61method run {} { 62 if {$fetch_spec ne {}} { 63 global M1B 64 65 # We were asked to refresh a single tracking branch 66 # before we get to work. We should do that before we 67 # consider any ref updating. 68 # 69 set fetch_ok 0 70 set l_trck [lindex $fetch_spec 0] 71 set remote [lindex $fetch_spec 1] 72 set r_head [lindex $fetch_spec 2] 73 regsub ^refs/heads/ $r_head {} r_name 74 75 set cmd [list git fetch $remote] 76 if {$l_trck ne {}} { 77 lappend cmd +$r_head:$l_trck 78 } else { 79 lappend cmd $r_head 80 } 81 82 _toplevel $this {Refreshing Tracking Branch} 83 set w_cons [::console::embed \ 84 $w.console \ 85 [mc "Fetching %s from %s" $r_name $remote]] 86 pack $w.console -fill both -expand 1 87 $w_cons exec $cmd [cb _finish_fetch] 88 89 bind $w <$M1B-Key-w> break 90 bind $w <$M1B-Key-W> break 91 bind $w <Visibility> " 92 [list grab $w] 93 [list focus $w] 94 " 95 wm protocol $w WM_DELETE_WINDOW [cb _noop] 96 tkwait window $w 97 98 if {!$fetch_ok} { 99 delete_this 100 return 0 101 } 102 } 103 104 if {$new_ref ne {}} { 105 # If we have a ref we need to update it before we can 106 # proceed with a checkout (if one was enabled). 107 # 108 if {![_update_ref $this]} { 109 delete_this 110 return 0 111 } 112 } 113 114 if {$checkout} { 115 _checkout $this 116 return 1 117 } 118 119 delete_this 120 return 1 121} 122 123method _noop {} {} 124 125method _finish_fetch {ok} { 126 if {$ok} { 127 set l_trck [lindex $fetch_spec 0] 128 if {$l_trck eq {}} { 129 set l_trck FETCH_HEAD 130 } 131 if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} { 132 set ok 0 133 $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck] 134 $w_cons insert $err 135 } 136 } 137 138 $w_cons done $ok 139 set w_cons {} 140 wm protocol $w WM_DELETE_WINDOW {} 141 142 if {$ok} { 143 destroy $w 144 set w {} 145 } else { 146 button $w.close -text [mc Close] -command [list destroy $w] 147 pack $w.close -side bottom -anchor e -padx 10 -pady 10 148 } 149 150 set fetch_ok $ok 151} 152 153method _update_ref {} { 154 global null_sha1 current_branch repo_config 155 156 set ref $new_ref 157 set new $new_hash 158 159 set is_current 0 160 set rh refs/heads/ 161 set rn [string length $rh] 162 if {[string equal -length $rn $rh $ref]} { 163 set newbranch [string range $ref $rn end] 164 if {$current_branch eq $newbranch} { 165 set is_current 1 166 } 167 } else { 168 set newbranch $ref 169 } 170 171 if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} { 172 # Assume it does not exist, and that is what the error was. 173 # 174 if {!$create} { 175 _error $this [mc "Branch '%s' does not exist." $newbranch] 176 return 0 177 } 178 179 set reflog_msg "branch: Created from $new_expr" 180 set cur $null_sha1 181 182 if {($repo_config(branch.autosetupmerge) eq {true} 183 || $repo_config(branch.autosetupmerge) eq {always}) 184 && $remote_source ne {} 185 && "refs/heads/$newbranch" eq $ref} { 186 187 set c_remote [lindex $remote_source 1] 188 set c_merge [lindex $remote_source 2] 189 if {[catch { 190 git config branch.$newbranch.remote $c_remote 191 git config branch.$newbranch.merge $c_merge 192 } err]} { 193 _error $this [strcat \ 194 [mc "Failed to configure simplified git-pull for '%s'." $newbranch] \ 195 "\n\n$err"] 196 } 197 } 198 } elseif {$create && $merge_type eq {none}} { 199 # We were told to create it, but not do a merge. 200 # Bad. Name shouldn't have existed. 201 # 202 _error $this [mc "Branch '%s' already exists." $newbranch] 203 return 0 204 } elseif {!$create && $merge_type eq {none}} { 205 # We aren't creating, it exists and we don't merge. 206 # We are probably just a simple branch switch. 207 # Use whatever value we just read. 208 # 209 set new $cur 210 set new_hash $cur 211 } elseif {$new eq $cur} { 212 # No merge would be required, don't compute anything. 213 # 214 } else { 215 catch {set merge_base [git merge-base $new $cur]} 216 if {$merge_base eq $cur} { 217 # The current branch is older. 218 # 219 set reflog_msg "merge $new_expr: Fast-forward" 220 } else { 221 switch -- $merge_type { 222 ff { 223 if {$merge_base eq $new} { 224 # The current branch is actually newer. 225 # 226 set new $cur 227 set new_hash $cur 228 } else { 229 _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr] 230 return 0 231 } 232 } 233 reset { 234 # The current branch will lose things. 235 # 236 if {[_confirm_reset $this $cur]} { 237 set reflog_msg "reset $new_expr" 238 } else { 239 return 0 240 } 241 } 242 default { 243 _error $this [mc "Merge strategy '%s' not supported." $merge_type] 244 return 0 245 } 246 } 247 } 248 } 249 250 if {$new ne $cur} { 251 if {$is_current} { 252 # No so fast. We should defer this in case 253 # we cannot update the working directory. 254 # 255 set update_old $cur 256 return 1 257 } 258 259 if {[catch { 260 git update-ref -m $reflog_msg $ref $new $cur 261 } err]} { 262 _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"] 263 return 0 264 } 265 } 266 267 return 1 268} 269 270method _checkout {} { 271 if {[lock_index checkout_op]} { 272 after idle [cb _start_checkout] 273 } else { 274 _error $this [mc "Staging area (index) is already locked."] 275 delete_this 276 } 277} 278 279method _start_checkout {} { 280 global HEAD commit_type 281 282 # -- Our in memory state should match the repository. 283 # 284 repository_state curType old_hash curMERGE_HEAD 285 if {[string match amend* $commit_type] 286 && $curType eq {normal} 287 && $old_hash eq $HEAD} { 288 } elseif {$commit_type ne $curType || $HEAD ne $old_hash} { 289 info_popup [mc "Last scanned state does not match repository state. 290 291Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed. 292 293The rescan will be automatically started now. 294"] 295 unlock_index 296 rescan ui_ready 297 delete_this 298 return 299 } 300 301 if {$old_hash eq $new_hash} { 302 _after_readtree $this 303 } elseif {[is_config_true gui.trustmtime]} { 304 _readtree $this 305 } else { 306 ui_status [mc "Refreshing file status..."] 307 set fd [git_read update-index \ 308 -q \ 309 --unmerged \ 310 --ignore-missing \ 311 --refresh \ 312 ] 313 fconfigure $fd -blocking 0 -translation binary 314 fileevent $fd readable [cb _refresh_wait $fd] 315 } 316} 317 318method _refresh_wait {fd} { 319 read $fd 320 if {[eof $fd]} { 321 close $fd 322 _readtree $this 323 } 324} 325 326method _name {} { 327 if {$new_ref eq {}} { 328 return [string range $new_hash 0 7] 329 } 330 331 set rh refs/heads/ 332 set rn [string length $rh] 333 if {[string equal -length $rn $rh $new_ref]} { 334 return [string range $new_ref $rn end] 335 } else { 336 return $new_ref 337 } 338} 339 340method _readtree {} { 341 global HEAD 342 343 set readtree_d {} 344 set status_bar_operation [$::main_status start \ 345 [mc "Updating working directory to '%s'..." [_name $this]] \ 346 [mc "files checked out"]] 347 348 set fd [git_read --stderr read-tree \ 349 -m \ 350 -u \ 351 -v \ 352 --exclude-per-directory=.gitignore \ 353 $HEAD \ 354 $new_hash \ 355 ] 356 fconfigure $fd -blocking 0 -translation binary 357 fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation] 358} 359 360method _readtree_wait {fd status_bar_operation} { 361 global current_branch 362 363 set buf [read $fd] 364 $status_bar_operation update_meter $buf 365 append readtree_d $buf 366 367 fconfigure $fd -blocking 1 368 if {![eof $fd]} { 369 fconfigure $fd -blocking 0 370 $status_bar_operation stop 371 return 372 } 373 374 if {[catch {close $fd}]} { 375 set err $readtree_d 376 regsub {^fatal: } $err {} err 377 $status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]] 378 warn_popup [strcat [mc "File level merge required."] " 379 380$err 381 382" [mc "Staying on branch '%s'." $current_branch]] 383 unlock_index 384 delete_this 385 return 386 } 387 388 $status_bar_operation stop 389 _after_readtree $this 390} 391 392method _after_readtree {} { 393 global commit_type HEAD MERGE_HEAD PARENT 394 global current_branch is_detached 395 global ui_comm 396 397 set name [_name $this] 398 set log "checkout: moving" 399 if {!$is_detached} { 400 append log " from $current_branch" 401 } 402 403 # -- Move/create HEAD as a symbolic ref. Core git does not 404 # even check for failure here, it Just Works(tm). If it 405 # doesn't we are in some really ugly state that is difficult 406 # to recover from within git-gui. 407 # 408 set rh refs/heads/ 409 set rn [string length $rh] 410 if {[string equal -length $rn $rh $new_ref]} { 411 set new_branch [string range $new_ref $rn end] 412 if {$is_detached || $current_branch ne $new_branch} { 413 append log " to $new_branch" 414 if {[catch { 415 git symbolic-ref -m $log HEAD $new_ref 416 } err]} { 417 _fatal $this $err 418 } 419 set current_branch $new_branch 420 set is_detached 0 421 } 422 } else { 423 if {!$is_detached || $new_hash ne $HEAD} { 424 append log " to $new_expr" 425 if {[catch { 426 _detach_HEAD $log $new_hash 427 } err]} { 428 _fatal $this $err 429 } 430 } 431 set current_branch HEAD 432 set is_detached 1 433 } 434 435 # -- We had to defer updating the branch itself until we 436 # knew the working directory would update. So now we 437 # need to finish that work. If it fails we're in big 438 # trouble. 439 # 440 if {$update_old ne {}} { 441 if {[catch { 442 git update-ref \ 443 -m $reflog_msg \ 444 $new_ref \ 445 $new_hash \ 446 $update_old 447 } err]} { 448 _fatal $this $err 449 } 450 } 451 452 if {$is_detached} { 453 info_popup [mc "You are no longer on a local branch. 454 455If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."] 456 } 457 458 # -- Run the post-checkout hook. 459 # 460 set fd_ph [githook_read post-checkout $old_hash $new_hash 1] 461 if {$fd_ph ne {}} { 462 global pch_error 463 set pch_error {} 464 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} 465 fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph] 466 } else { 467 _update_repo_state $this 468 } 469} 470 471method _postcheckout_wait {fd_ph} { 472 global pch_error 473 474 append pch_error [read $fd_ph] 475 fconfigure $fd_ph -blocking 1 476 if {[eof $fd_ph]} { 477 if {[catch {close $fd_ph}]} { 478 hook_failed_popup post-checkout $pch_error 0 479 } 480 unset pch_error 481 _update_repo_state $this 482 return 483 } 484 fconfigure $fd_ph -blocking 0 485} 486 487method _update_repo_state {} { 488 # -- Update our repository state. If we were previously in 489 # amend mode we need to toss the current buffer and do a 490 # full rescan to update our file lists. If we weren't in 491 # amend mode our file lists are accurate and we can avoid 492 # the rescan. 493 # 494 global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT 495 global ui_comm 496 497 unlock_index 498 set name [_name $this] 499 set commit_type_is_amend 0 500 if {[string match amend* $commit_type]} { 501 $ui_comm delete 0.0 end 502 $ui_comm edit reset 503 $ui_comm edit modified false 504 rescan [list ui_status [mc "Checked out '%s'." $name]] 505 } else { 506 repository_state commit_type HEAD MERGE_HEAD 507 set PARENT $HEAD 508 ui_status [mc "Checked out '%s'." $name] 509 } 510 delete_this 511} 512 513git-version proc _detach_HEAD {log new} { 514 >= 1.5.3 { 515 git update-ref --no-deref -m $log HEAD $new 516 } 517 default { 518 set p [gitdir HEAD] 519 file delete $p 520 set fd [open $p w] 521 fconfigure $fd -translation lf -encoding utf-8 522 puts $fd $new 523 close $fd 524 } 525} 526 527method _confirm_reset {cur} { 528 set reset_ok 0 529 set name [_name $this] 530 set gitk [list do_gitk [list $cur ^$new_hash]] 531 532 _toplevel $this {Confirm Branch Reset} 533 pack [label $w.msg1 \ 534 -anchor w \ 535 -justify left \ 536 -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\ 537 ] -anchor w 538 539 set list $w.list.l 540 frame $w.list 541 text $list \ 542 -font font_diff \ 543 -width 80 \ 544 -height 10 \ 545 -wrap none \ 546 -xscrollcommand [list $w.list.sbx set] \ 547 -yscrollcommand [list $w.list.sby set] 548 scrollbar $w.list.sbx -orient h -command [list $list xview] 549 scrollbar $w.list.sby -orient v -command [list $list yview] 550 pack $w.list.sbx -fill x -side bottom 551 pack $w.list.sby -fill y -side right 552 pack $list -fill both -expand 1 553 pack $w.list -fill both -expand 1 -padx 5 -pady 5 554 555 pack [label $w.msg2 \ 556 -anchor w \ 557 -justify left \ 558 -text [mc "Recovering lost commits may not be easy."] \ 559 ] 560 pack [label $w.msg3 \ 561 -anchor w \ 562 -justify left \ 563 -text [mc "Reset '%s'?" $name] \ 564 ] 565 566 frame $w.buttons 567 button $w.buttons.visualize \ 568 -text [mc Visualize] \ 569 -command $gitk 570 pack $w.buttons.visualize -side left 571 button $w.buttons.reset \ 572 -text [mc Reset] \ 573 -command " 574 set @reset_ok 1 575 destroy $w 576 " 577 pack $w.buttons.reset -side right 578 button $w.buttons.cancel \ 579 -default active \ 580 -text [mc Cancel] \ 581 -command [list destroy $w] 582 pack $w.buttons.cancel -side right -padx 5 583 pack $w.buttons -side bottom -fill x -pady 10 -padx 10 584 585 set fd [git_read rev-list --pretty=oneline $cur ^$new_hash] 586 while {[gets $fd line] > 0} { 587 set abbr [string range $line 0 7] 588 set subj [string range $line 41 end] 589 $list insert end "$abbr $subj\n" 590 } 591 close $fd 592 $list configure -state disabled 593 594 bind $w <Key-v> $gitk 595 bind $w <Visibility> " 596 grab $w 597 focus $w.buttons.cancel 598 " 599 bind $w <Key-Return> [list destroy $w] 600 bind $w <Key-Escape> [list destroy $w] 601 tkwait window $w 602 return $reset_ok 603} 604 605method _error {msg} { 606 if {[winfo ismapped $parent_w]} { 607 set p $parent_w 608 } else { 609 set p . 610 } 611 612 tk_messageBox \ 613 -icon error \ 614 -type ok \ 615 -title [wm title $p] \ 616 -parent $p \ 617 -message $msg 618} 619 620method _toplevel {title} { 621 regsub -all {::} $this {__} w 622 set w .$w 623 624 if {[winfo ismapped $parent_w]} { 625 set p $parent_w 626 } else { 627 set p . 628 } 629 630 toplevel $w 631 wm title $w $title 632 wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]" 633} 634 635method _fatal {err} { 636 error_popup [strcat [mc "Failed to set current branch. 637 638This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file. 639 640This should not have occurred. %s will now close and give up." [appname]] " 641 642$err"] 643 exit 1 644} 645 646} 647