| 1 | # git-gui commit checkout support |
| 2 | # Copyright (C) 2007 Shawn Pearce |
| 3 | |
| 4 | class checkout_op { |
| 5 | |
| 6 | field w {}; # our window (if we have one) |
| 7 | field w_cons {}; # embedded console window object |
| 8 | |
| 9 | field new_expr ; # expression the user saw/thinks this is |
| 10 | field new_hash ; # commit SHA-1 we are switching to |
| 11 | field new_ref ; # ref we are updating/creating |
| 12 | field old_hash ; # commit SHA-1 that was checked out when we started |
| 13 | |
| 14 | field parent_w .; # window that started us |
| 15 | field merge_type none; # type of merge to apply to existing branch |
| 16 | field merge_base {}; # merge base if we have another ref involved |
| 17 | field fetch_spec {}; # refetch tracking branch if used? |
| 18 | field checkout 1; # actually checkout the branch? |
| 19 | field create 0; # create the branch if it doesn't exist? |
| 20 | field remote_source {}; # same as fetch_spec, to setup tracking |
| 21 | |
| 22 | field reset_ok 0; # did the user agree to reset? |
| 23 | field fetch_ok 0; # did the fetch succeed? |
| 24 | |
| 25 | field readtree_d {}; # buffered output from read-tree |
| 26 | field update_old {}; # was the update-ref call deferred? |
| 27 | field reflog_msg {}; # log message for the update-ref call |
| 28 | |
| 29 | constructor 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 | |
| 37 | method parent {path} { |
| 38 | set parent_w [winfo toplevel $path] |
| 39 | } |
| 40 | |
| 41 | method enable_merge {type} { |
| 42 | set merge_type $type |
| 43 | } |
| 44 | |
| 45 | method enable_fetch {spec} { |
| 46 | set fetch_spec $spec |
| 47 | } |
| 48 | |
| 49 | method remote_source {spec} { |
| 50 | set remote_source $spec |
| 51 | } |
| 52 | |
| 53 | method enable_checkout {co} { |
| 54 | set checkout $co |
| 55 | } |
| 56 | |
| 57 | method enable_create {co} { |
| 58 | set create $co |
| 59 | } |
| 60 | |
| 61 | method 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 | |
| 123 | method _noop {} {} |
| 124 | |
| 125 | method _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 | |
| 153 | method _update_ref {} { |
| 154 | global nullid 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 $nullid |
| 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 | |
| 270 | method _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 | |
| 279 | method _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 | |
| 291 | Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed. |
| 292 | |
| 293 | The 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 [list 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 | |
| 318 | method _refresh_wait {fd} { |
| 319 | read $fd |
| 320 | if {[eof $fd]} { |
| 321 | close $fd |
| 322 | _readtree $this |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | method _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 | |
| 340 | method _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 [list read-tree \ |
| 349 | -m \ |
| 350 | -u \ |
| 351 | -v \ |
| 352 | --exclude-per-directory=.gitignore \ |
| 353 | $HEAD \ |
| 354 | $new_hash \ |
| 355 | ] \ |
| 356 | [list 2>@1]] |
| 357 | fconfigure $fd -blocking 0 -translation binary |
| 358 | fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation] |
| 359 | } |
| 360 | |
| 361 | method _readtree_wait {fd status_bar_operation} { |
| 362 | global current_branch |
| 363 | |
| 364 | set buf [read $fd] |
| 365 | $status_bar_operation update_meter $buf |
| 366 | append readtree_d $buf |
| 367 | |
| 368 | fconfigure $fd -blocking 1 |
| 369 | if {![eof $fd]} { |
| 370 | fconfigure $fd -blocking 0 |
| 371 | $status_bar_operation stop |
| 372 | return |
| 373 | } |
| 374 | |
| 375 | if {[catch {close $fd}]} { |
| 376 | set err $readtree_d |
| 377 | regsub {^fatal: } $err {} err |
| 378 | $status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]] |
| 379 | warn_popup [strcat [mc "File level merge required."] " |
| 380 | |
| 381 | $err |
| 382 | |
| 383 | " [mc "Staying on branch '%s'." $current_branch]] |
| 384 | unlock_index |
| 385 | delete_this |
| 386 | return |
| 387 | } |
| 388 | |
| 389 | $status_bar_operation stop |
| 390 | _after_readtree $this |
| 391 | } |
| 392 | |
| 393 | method _after_readtree {} { |
| 394 | global commit_type HEAD MERGE_HEAD PARENT |
| 395 | global current_branch is_detached |
| 396 | global ui_comm |
| 397 | |
| 398 | set name [_name $this] |
| 399 | set log "checkout: moving" |
| 400 | if {!$is_detached} { |
| 401 | append log " from $current_branch" |
| 402 | } |
| 403 | |
| 404 | # -- Move/create HEAD as a symbolic ref. Core git does not |
| 405 | # even check for failure here, it Just Works(tm). If it |
| 406 | # doesn't we are in some really ugly state that is difficult |
| 407 | # to recover from within git-gui. |
| 408 | # |
| 409 | set rh refs/heads/ |
| 410 | set rn [string length $rh] |
| 411 | if {[string equal -length $rn $rh $new_ref]} { |
| 412 | set new_branch [string range $new_ref $rn end] |
| 413 | if {$is_detached || $current_branch ne $new_branch} { |
| 414 | append log " to $new_branch" |
| 415 | if {[catch { |
| 416 | git symbolic-ref -m $log HEAD $new_ref |
| 417 | } err]} { |
| 418 | _fatal $this $err |
| 419 | } |
| 420 | set current_branch $new_branch |
| 421 | set is_detached 0 |
| 422 | } |
| 423 | } else { |
| 424 | if {!$is_detached || $new_hash ne $HEAD} { |
| 425 | append log " to $new_expr" |
| 426 | if {[catch { |
| 427 | _detach_HEAD $log $new_hash |
| 428 | } err]} { |
| 429 | _fatal $this $err |
| 430 | } |
| 431 | } |
| 432 | set current_branch HEAD |
| 433 | set is_detached 1 |
| 434 | } |
| 435 | |
| 436 | # -- We had to defer updating the branch itself until we |
| 437 | # knew the working directory would update. So now we |
| 438 | # need to finish that work. If it fails we're in big |
| 439 | # trouble. |
| 440 | # |
| 441 | if {$update_old ne {}} { |
| 442 | if {[catch { |
| 443 | git update-ref \ |
| 444 | -m $reflog_msg \ |
| 445 | $new_ref \ |
| 446 | $new_hash \ |
| 447 | $update_old |
| 448 | } err]} { |
| 449 | _fatal $this $err |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | if {$is_detached} { |
| 454 | info_popup [mc "You are no longer on a local branch. |
| 455 | |
| 456 | If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."] |
| 457 | } |
| 458 | |
| 459 | # -- Run the post-checkout hook. |
| 460 | # |
| 461 | set fd_ph [githook_read post-checkout $old_hash $new_hash 1] |
| 462 | if {$fd_ph ne {}} { |
| 463 | global pch_error |
| 464 | set pch_error {} |
| 465 | fconfigure $fd_ph -blocking 0 -translation binary |
| 466 | fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph] |
| 467 | } else { |
| 468 | _update_repo_state $this |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | method _postcheckout_wait {fd_ph} { |
| 473 | global pch_error |
| 474 | |
| 475 | append pch_error [read $fd_ph] |
| 476 | fconfigure $fd_ph -blocking 1 |
| 477 | if {[eof $fd_ph]} { |
| 478 | if {[catch {close $fd_ph}]} { |
| 479 | hook_failed_popup post-checkout $pch_error 0 |
| 480 | } |
| 481 | unset pch_error |
| 482 | _update_repo_state $this |
| 483 | return |
| 484 | } |
| 485 | fconfigure $fd_ph -blocking 0 |
| 486 | } |
| 487 | |
| 488 | method _update_repo_state {} { |
| 489 | # -- Update our repository state. If we were previously in |
| 490 | # amend mode we need to toss the current buffer and do a |
| 491 | # full rescan to update our file lists. If we weren't in |
| 492 | # amend mode our file lists are accurate and we can avoid |
| 493 | # the rescan. |
| 494 | # |
| 495 | global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT |
| 496 | global ui_comm |
| 497 | |
| 498 | unlock_index |
| 499 | set name [_name $this] |
| 500 | set commit_type_is_amend 0 |
| 501 | if {[string match amend* $commit_type]} { |
| 502 | $ui_comm delete 0.0 end |
| 503 | $ui_comm edit reset |
| 504 | $ui_comm edit modified false |
| 505 | rescan [list ui_status [mc "Checked out '%s'." $name]] |
| 506 | } else { |
| 507 | repository_state commit_type HEAD MERGE_HEAD |
| 508 | set PARENT $HEAD |
| 509 | ui_status [mc "Checked out '%s'." $name] |
| 510 | } |
| 511 | delete_this |
| 512 | } |
| 513 | |
| 514 | proc _detach_HEAD {log new} { |
| 515 | git update-ref --no-deref -m $log HEAD $new |
| 516 | } |
| 517 | |
| 518 | method _confirm_reset {cur} { |
| 519 | set reset_ok 0 |
| 520 | set name [_name $this] |
| 521 | set gitk [list do_gitk [list $cur ^$new_hash]] |
| 522 | |
| 523 | _toplevel $this {Confirm Branch Reset} |
| 524 | pack [label $w.msg1 \ |
| 525 | -anchor w \ |
| 526 | -justify left \ |
| 527 | -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\ |
| 528 | ] -anchor w |
| 529 | |
| 530 | set list $w.list.l |
| 531 | frame $w.list |
| 532 | text $list \ |
| 533 | -font font_diff \ |
| 534 | -width 80 \ |
| 535 | -height 10 \ |
| 536 | -wrap none \ |
| 537 | -xscrollcommand [list $w.list.sbx set] \ |
| 538 | -yscrollcommand [list $w.list.sby set] |
| 539 | scrollbar $w.list.sbx -orient h -command [list $list xview] |
| 540 | scrollbar $w.list.sby -orient v -command [list $list yview] |
| 541 | pack $w.list.sbx -fill x -side bottom |
| 542 | pack $w.list.sby -fill y -side right |
| 543 | pack $list -fill both -expand 1 |
| 544 | pack $w.list -fill both -expand 1 -padx 5 -pady 5 |
| 545 | |
| 546 | pack [label $w.msg2 \ |
| 547 | -anchor w \ |
| 548 | -justify left \ |
| 549 | -text [mc "Recovering lost commits may not be easy."] \ |
| 550 | ] |
| 551 | pack [label $w.msg3 \ |
| 552 | -anchor w \ |
| 553 | -justify left \ |
| 554 | -text [mc "Reset '%s'?" $name] \ |
| 555 | ] |
| 556 | |
| 557 | frame $w.buttons |
| 558 | button $w.buttons.visualize \ |
| 559 | -text [mc Visualize] \ |
| 560 | -command $gitk |
| 561 | pack $w.buttons.visualize -side left |
| 562 | button $w.buttons.reset \ |
| 563 | -text [mc Reset] \ |
| 564 | -command " |
| 565 | set @reset_ok 1 |
| 566 | destroy $w |
| 567 | " |
| 568 | pack $w.buttons.reset -side right |
| 569 | button $w.buttons.cancel \ |
| 570 | -default active \ |
| 571 | -text [mc Cancel] \ |
| 572 | -command [list destroy $w] |
| 573 | pack $w.buttons.cancel -side right -padx 5 |
| 574 | pack $w.buttons -side bottom -fill x -pady 10 -padx 10 |
| 575 | |
| 576 | set fd [git_read [list rev-list --pretty=oneline $cur ^$new_hash]] |
| 577 | while {[gets $fd line] > 0} { |
| 578 | set abbr [string range $line 0 7] |
| 579 | set subj [string range $line 41 end] |
| 580 | $list insert end "$abbr $subj\n" |
| 581 | } |
| 582 | close $fd |
| 583 | $list configure -state disabled |
| 584 | |
| 585 | bind $w <Key-v> $gitk |
| 586 | bind $w <Visibility> " |
| 587 | grab $w |
| 588 | focus $w.buttons.cancel |
| 589 | " |
| 590 | bind $w <Key-Return> [list destroy $w] |
| 591 | bind $w <Key-Escape> [list destroy $w] |
| 592 | tkwait window $w |
| 593 | return $reset_ok |
| 594 | } |
| 595 | |
| 596 | method _error {msg} { |
| 597 | if {[winfo ismapped $parent_w]} { |
| 598 | set p $parent_w |
| 599 | } else { |
| 600 | set p . |
| 601 | } |
| 602 | |
| 603 | tk_messageBox \ |
| 604 | -icon error \ |
| 605 | -type ok \ |
| 606 | -title [wm title $p] \ |
| 607 | -parent $p \ |
| 608 | -message $msg |
| 609 | } |
| 610 | |
| 611 | method _toplevel {title} { |
| 612 | regsub -all {::} $this {__} w |
| 613 | set w .$w |
| 614 | |
| 615 | if {[winfo ismapped $parent_w]} { |
| 616 | set p $parent_w |
| 617 | } else { |
| 618 | set p . |
| 619 | } |
| 620 | |
| 621 | toplevel $w |
| 622 | wm title $w $title |
| 623 | wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]" |
| 624 | } |
| 625 | |
| 626 | method _fatal {err} { |
| 627 | error_popup [strcat [mc "Failed to set current branch. |
| 628 | |
| 629 | This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file. |
| 630 | |
| 631 | This should not have occurred. %s will now close and give up." [appname]] " |
| 632 | |
| 633 | $err"] |
| 634 | exit 1 |
| 635 | } |
| 636 | |
| 637 | } |