lib / branch.tclon commit git-gui: Refactor into multiple files to save my sanity (f522c9b)
   1# git-gui branch (create/delete) support
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4proc load_all_heads {} {
   5        global all_heads
   6
   7        set all_heads [list]
   8        set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
   9        while {[gets $fd line] > 0} {
  10                if {[is_tracking_branch $line]} continue
  11                if {![regsub ^refs/heads/ $line {} name]} continue
  12                lappend all_heads $name
  13        }
  14        close $fd
  15
  16        set all_heads [lsort $all_heads]
  17}
  18
  19proc load_all_tags {} {
  20        set all_tags [list]
  21        set fd [open "| git for-each-ref --format=%(refname) refs/tags" r]
  22        while {[gets $fd line] > 0} {
  23                if {![regsub ^refs/tags/ $line {} name]} continue
  24                lappend all_tags $name
  25        }
  26        close $fd
  27
  28        return [lsort $all_tags]
  29}
  30
  31proc populate_branch_menu {} {
  32        global all_heads disable_on_lock
  33
  34        set m .mbar.branch
  35        set last [$m index last]
  36        for {set i 0} {$i <= $last} {incr i} {
  37                if {[$m type $i] eq {separator}} {
  38                        $m delete $i last
  39                        set new_dol [list]
  40                        foreach a $disable_on_lock {
  41                                if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
  42                                        lappend new_dol $a
  43                                }
  44                        }
  45                        set disable_on_lock $new_dol
  46                        break
  47                }
  48        }
  49
  50        if {$all_heads ne {}} {
  51                $m add separator
  52        }
  53        foreach b $all_heads {
  54                $m add radiobutton \
  55                        -label $b \
  56                        -command [list switch_branch $b] \
  57                        -variable current_branch \
  58                        -value $b
  59                lappend disable_on_lock \
  60                        [list $m entryconf [$m index last] -state]
  61        }
  62}
  63
  64proc do_create_branch_action {w} {
  65        global all_heads null_sha1 repo_config
  66        global create_branch_checkout create_branch_revtype
  67        global create_branch_head create_branch_trackinghead
  68        global create_branch_name create_branch_revexp
  69        global create_branch_tag
  70
  71        set newbranch $create_branch_name
  72        if {$newbranch eq {}
  73                || $newbranch eq $repo_config(gui.newbranchtemplate)} {
  74                tk_messageBox \
  75                        -icon error \
  76                        -type ok \
  77                        -title [wm title $w] \
  78                        -parent $w \
  79                        -message "Please supply a branch name."
  80                focus $w.desc.name_t
  81                return
  82        }
  83        if {![catch {git show-ref --verify -- "refs/heads/$newbranch"}]} {
  84                tk_messageBox \
  85                        -icon error \
  86                        -type ok \
  87                        -title [wm title $w] \
  88                        -parent $w \
  89                        -message "Branch '$newbranch' already exists."
  90                focus $w.desc.name_t
  91                return
  92        }
  93        if {[catch {git check-ref-format "heads/$newbranch"}]} {
  94                tk_messageBox \
  95                        -icon error \
  96                        -type ok \
  97                        -title [wm title $w] \
  98                        -parent $w \
  99                        -message "We do not like '$newbranch' as a branch name."
 100                focus $w.desc.name_t
 101                return
 102        }
 103
 104        set rev {}
 105        switch -- $create_branch_revtype {
 106        head {set rev $create_branch_head}
 107        tracking {set rev $create_branch_trackinghead}
 108        tag {set rev $create_branch_tag}
 109        expression {set rev $create_branch_revexp}
 110        }
 111        if {[catch {set cmt [git rev-parse --verify "${rev}^0"]}]} {
 112                tk_messageBox \
 113                        -icon error \
 114                        -type ok \
 115                        -title [wm title $w] \
 116                        -parent $w \
 117                        -message "Invalid starting revision: $rev"
 118                return
 119        }
 120        if {[catch {
 121                        git update-ref \
 122                                -m "branch: Created from $rev" \
 123                                "refs/heads/$newbranch" \
 124                                $cmt \
 125                                $null_sha1
 126                } err]} {
 127                tk_messageBox \
 128                        -icon error \
 129                        -type ok \
 130                        -title [wm title $w] \
 131                        -parent $w \
 132                        -message "Failed to create '$newbranch'.\n\n$err"
 133                return
 134        }
 135
 136        lappend all_heads $newbranch
 137        set all_heads [lsort $all_heads]
 138        populate_branch_menu
 139        destroy $w
 140        if {$create_branch_checkout} {
 141                switch_branch $newbranch
 142        }
 143}
 144
 145proc radio_selector {varname value args} {
 146        upvar #0 $varname var
 147        set var $value
 148}
 149
 150trace add variable create_branch_head write \
 151        [list radio_selector create_branch_revtype head]
 152trace add variable create_branch_trackinghead write \
 153        [list radio_selector create_branch_revtype tracking]
 154trace add variable create_branch_tag write \
 155        [list radio_selector create_branch_revtype tag]
 156
 157trace add variable delete_branch_head write \
 158        [list radio_selector delete_branch_checktype head]
 159trace add variable delete_branch_trackinghead write \
 160        [list radio_selector delete_branch_checktype tracking]
 161
 162proc do_create_branch {} {
 163        global all_heads current_branch repo_config
 164        global create_branch_checkout create_branch_revtype
 165        global create_branch_head create_branch_trackinghead
 166        global create_branch_name create_branch_revexp
 167        global create_branch_tag
 168
 169        set w .branch_editor
 170        toplevel $w
 171        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
 172
 173        label $w.header -text {Create New Branch} \
 174                -font font_uibold
 175        pack $w.header -side top -fill x
 176
 177        frame $w.buttons
 178        button $w.buttons.create -text Create \
 179                -default active \
 180                -command [list do_create_branch_action $w]
 181        pack $w.buttons.create -side right
 182        button $w.buttons.cancel -text {Cancel} \
 183                -command [list destroy $w]
 184        pack $w.buttons.cancel -side right -padx 5
 185        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 186
 187        labelframe $w.desc -text {Branch Description}
 188        label $w.desc.name_l -text {Name:}
 189        entry $w.desc.name_t \
 190                -borderwidth 1 \
 191                -relief sunken \
 192                -width 40 \
 193                -textvariable create_branch_name \
 194                -validate key \
 195                -validatecommand {
 196                        if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
 197                        return 1
 198                }
 199        grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
 200        grid columnconfigure $w.desc 1 -weight 1
 201        pack $w.desc -anchor nw -fill x -pady 5 -padx 5
 202
 203        labelframe $w.from -text {Starting Revision}
 204        radiobutton $w.from.head_r \
 205                -text {Local Branch:} \
 206                -value head \
 207                -variable create_branch_revtype
 208        eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
 209        grid $w.from.head_r $w.from.head_m -sticky w
 210        set all_trackings [all_tracking_branches]
 211        if {$all_trackings ne {}} {
 212                set create_branch_trackinghead [lindex $all_trackings 0]
 213                radiobutton $w.from.tracking_r \
 214                        -text {Tracking Branch:} \
 215                        -value tracking \
 216                        -variable create_branch_revtype
 217                eval tk_optionMenu $w.from.tracking_m \
 218                        create_branch_trackinghead \
 219                        $all_trackings
 220                grid $w.from.tracking_r $w.from.tracking_m -sticky w
 221        }
 222        set all_tags [load_all_tags]
 223        if {$all_tags ne {}} {
 224                set create_branch_tag [lindex $all_tags 0]
 225                radiobutton $w.from.tag_r \
 226                        -text {Tag:} \
 227                        -value tag \
 228                        -variable create_branch_revtype
 229                eval tk_optionMenu $w.from.tag_m create_branch_tag $all_tags
 230                grid $w.from.tag_r $w.from.tag_m -sticky w
 231        }
 232        radiobutton $w.from.exp_r \
 233                -text {Revision Expression:} \
 234                -value expression \
 235                -variable create_branch_revtype
 236        entry $w.from.exp_t \
 237                -borderwidth 1 \
 238                -relief sunken \
 239                -width 50 \
 240                -textvariable create_branch_revexp \
 241                -validate key \
 242                -validatecommand {
 243                        if {%d == 1 && [regexp {\s} %S]} {return 0}
 244                        if {%d == 1 && [string length %S] > 0} {
 245                                set create_branch_revtype expression
 246                        }
 247                        return 1
 248                }
 249        grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
 250        grid columnconfigure $w.from 1 -weight 1
 251        pack $w.from -anchor nw -fill x -pady 5 -padx 5
 252
 253        labelframe $w.postActions -text {Post Creation Actions}
 254        checkbutton $w.postActions.checkout \
 255                -text {Checkout after creation} \
 256                -variable create_branch_checkout
 257        pack $w.postActions.checkout -anchor nw
 258        pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
 259
 260        set create_branch_checkout 1
 261        set create_branch_head $current_branch
 262        set create_branch_revtype head
 263        set create_branch_name $repo_config(gui.newbranchtemplate)
 264        set create_branch_revexp {}
 265
 266        bind $w <Visibility> "
 267                grab $w
 268                $w.desc.name_t icursor end
 269                focus $w.desc.name_t
 270        "
 271        bind $w <Key-Escape> "destroy $w"
 272        bind $w <Key-Return> "do_create_branch_action $w;break"
 273        wm title $w "[appname] ([reponame]): Create Branch"
 274        tkwait window $w
 275}
 276
 277proc do_delete_branch_action {w} {
 278        global all_heads
 279        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
 280
 281        set check_rev {}
 282        switch -- $delete_branch_checktype {
 283        head {set check_rev $delete_branch_head}
 284        tracking {set check_rev $delete_branch_trackinghead}
 285        always {set check_rev {:none}}
 286        }
 287        if {$check_rev eq {:none}} {
 288                set check_cmt {}
 289        } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
 290                tk_messageBox \
 291                        -icon error \
 292                        -type ok \
 293                        -title [wm title $w] \
 294                        -parent $w \
 295                        -message "Invalid check revision: $check_rev"
 296                return
 297        }
 298
 299        set to_delete [list]
 300        set not_merged [list]
 301        foreach i [$w.list.l curselection] {
 302                set b [$w.list.l get $i]
 303                if {[catch {set o [git rev-parse --verify $b]}]} continue
 304                if {$check_cmt ne {}} {
 305                        if {$b eq $check_rev} continue
 306                        if {[catch {set m [git merge-base $o $check_cmt]}]} continue
 307                        if {$o ne $m} {
 308                                lappend not_merged $b
 309                                continue
 310                        }
 311                }
 312                lappend to_delete [list $b $o]
 313        }
 314        if {$not_merged ne {}} {
 315                set msg "The following branches are not completely merged into $check_rev:
 316
 317 - [join $not_merged "\n - "]"
 318                tk_messageBox \
 319                        -icon info \
 320                        -type ok \
 321                        -title [wm title $w] \
 322                        -parent $w \
 323                        -message $msg
 324        }
 325        if {$to_delete eq {}} return
 326        if {$delete_branch_checktype eq {always}} {
 327                set msg {Recovering deleted branches is difficult.
 328
 329Delete the selected branches?}
 330                if {[tk_messageBox \
 331                        -icon warning \
 332                        -type yesno \
 333                        -title [wm title $w] \
 334                        -parent $w \
 335                        -message $msg] ne yes} {
 336                        return
 337                }
 338        }
 339
 340        set failed {}
 341        foreach i $to_delete {
 342                set b [lindex $i 0]
 343                set o [lindex $i 1]
 344                if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
 345                        append failed " - $b: $err\n"
 346                } else {
 347                        set x [lsearch -sorted -exact $all_heads $b]
 348                        if {$x >= 0} {
 349                                set all_heads [lreplace $all_heads $x $x]
 350                        }
 351                }
 352        }
 353
 354        if {$failed ne {}} {
 355                tk_messageBox \
 356                        -icon error \
 357                        -type ok \
 358                        -title [wm title $w] \
 359                        -parent $w \
 360                        -message "Failed to delete branches:\n$failed"
 361        }
 362
 363        set all_heads [lsort $all_heads]
 364        populate_branch_menu
 365        destroy $w
 366}
 367
 368proc do_delete_branch {} {
 369        global all_heads tracking_branches current_branch
 370        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
 371
 372        set w .branch_editor
 373        toplevel $w
 374        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
 375
 376        label $w.header -text {Delete Local Branch} \
 377                -font font_uibold
 378        pack $w.header -side top -fill x
 379
 380        frame $w.buttons
 381        button $w.buttons.create -text Delete \
 382                -command [list do_delete_branch_action $w]
 383        pack $w.buttons.create -side right
 384        button $w.buttons.cancel -text {Cancel} \
 385                -command [list destroy $w]
 386        pack $w.buttons.cancel -side right -padx 5
 387        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 388
 389        labelframe $w.list -text {Local Branches}
 390        listbox $w.list.l \
 391                -height 10 \
 392                -width 70 \
 393                -selectmode extended \
 394                -yscrollcommand [list $w.list.sby set]
 395        foreach h $all_heads {
 396                if {$h ne $current_branch} {
 397                        $w.list.l insert end $h
 398                }
 399        }
 400        scrollbar $w.list.sby -command [list $w.list.l yview]
 401        pack $w.list.sby -side right -fill y
 402        pack $w.list.l -side left -fill both -expand 1
 403        pack $w.list -fill both -expand 1 -pady 5 -padx 5
 404
 405        labelframe $w.validate -text {Delete Only If}
 406        radiobutton $w.validate.head_r \
 407                -text {Merged Into Local Branch:} \
 408                -value head \
 409                -variable delete_branch_checktype
 410        eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
 411        grid $w.validate.head_r $w.validate.head_m -sticky w
 412        set all_trackings [all_tracking_branches]
 413        if {$all_trackings ne {}} {
 414                set delete_branch_trackinghead [lindex $all_trackings 0]
 415                radiobutton $w.validate.tracking_r \
 416                        -text {Merged Into Tracking Branch:} \
 417                        -value tracking \
 418                        -variable delete_branch_checktype
 419                eval tk_optionMenu $w.validate.tracking_m \
 420                        delete_branch_trackinghead \
 421                        $all_trackings
 422                grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
 423        }
 424        radiobutton $w.validate.always_r \
 425                -text {Always (Do not perform merge checks)} \
 426                -value always \
 427                -variable delete_branch_checktype
 428        grid $w.validate.always_r -columnspan 2 -sticky w
 429        grid columnconfigure $w.validate 1 -weight 1
 430        pack $w.validate -anchor nw -fill x -pady 5 -padx 5
 431
 432        set delete_branch_head $current_branch
 433        set delete_branch_checktype head
 434
 435        bind $w <Visibility> "grab $w; focus $w"
 436        bind $w <Key-Escape> "destroy $w"
 437        wm title $w "[appname] ([reponame]): Delete Branch"
 438        tkwait window $w
 439}
 440
 441proc switch_branch {new_branch} {
 442        global HEAD commit_type current_branch repo_config
 443
 444        if {![lock_index switch]} return
 445
 446        # -- Our in memory state should match the repository.
 447        #
 448        repository_state curType curHEAD curMERGE_HEAD
 449        if {[string match amend* $commit_type]
 450                && $curType eq {normal}
 451                && $curHEAD eq $HEAD} {
 452        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 453                info_popup {Last scanned state does not match repository state.
 454
 455Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
 456
 457The rescan will be automatically started now.
 458}
 459                unlock_index
 460                rescan {set ui_status_value {Ready.}}
 461                return
 462        }
 463
 464        # -- Don't do a pointless switch.
 465        #
 466        if {$current_branch eq $new_branch} {
 467                unlock_index
 468                return
 469        }
 470
 471        if {$repo_config(gui.trustmtime) eq {true}} {
 472                switch_branch_stage2 {} $new_branch
 473        } else {
 474                set ui_status_value {Refreshing file status...}
 475                set cmd [list git update-index]
 476                lappend cmd -q
 477                lappend cmd --unmerged
 478                lappend cmd --ignore-missing
 479                lappend cmd --refresh
 480                set fd_rf [open "| $cmd" r]
 481                fconfigure $fd_rf -blocking 0 -translation binary
 482                fileevent $fd_rf readable \
 483                        [list switch_branch_stage2 $fd_rf $new_branch]
 484        }
 485}
 486
 487proc switch_branch_stage2 {fd_rf new_branch} {
 488        global ui_status_value HEAD
 489
 490        if {$fd_rf ne {}} {
 491                read $fd_rf
 492                if {![eof $fd_rf]} return
 493                close $fd_rf
 494        }
 495
 496        set ui_status_value "Updating working directory to '$new_branch'..."
 497        set cmd [list git read-tree]
 498        lappend cmd -m
 499        lappend cmd -u
 500        lappend cmd --exclude-per-directory=.gitignore
 501        lappend cmd $HEAD
 502        lappend cmd $new_branch
 503        set fd_rt [open "| $cmd" r]
 504        fconfigure $fd_rt -blocking 0 -translation binary
 505        fileevent $fd_rt readable \
 506                [list switch_branch_readtree_wait $fd_rt $new_branch]
 507}
 508
 509proc switch_branch_readtree_wait {fd_rt new_branch} {
 510        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 511        global current_branch
 512        global ui_comm ui_status_value
 513
 514        # -- We never get interesting output on stdout; only stderr.
 515        #
 516        read $fd_rt
 517        fconfigure $fd_rt -blocking 1
 518        if {![eof $fd_rt]} {
 519                fconfigure $fd_rt -blocking 0
 520                return
 521        }
 522
 523        # -- The working directory wasn't in sync with the index and
 524        #    we'd have to overwrite something to make the switch. A
 525        #    merge is required.
 526        #
 527        if {[catch {close $fd_rt} err]} {
 528                regsub {^fatal: } $err {} err
 529                warn_popup "File level merge required.
 530
 531$err
 532
 533Staying on branch '$current_branch'."
 534                set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
 535                unlock_index
 536                return
 537        }
 538
 539        # -- Update the symbolic ref.  Core git doesn't even check for failure
 540        #    here, it Just Works(tm).  If it doesn't we are in some really ugly
 541        #    state that is difficult to recover from within git-gui.
 542        #
 543        if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
 544                error_popup "Failed to set current branch.
 545
 546This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
 547
 548This should not have occurred.  [appname] will now close and give up.
 549
 550$err"
 551                do_quit
 552                return
 553        }
 554
 555        # -- Update our repository state.  If we were previously in amend mode
 556        #    we need to toss the current buffer and do a full rescan to update
 557        #    our file lists.  If we weren't in amend mode our file lists are
 558        #    accurate and we can avoid the rescan.
 559        #
 560        unlock_index
 561        set selected_commit_type new
 562        if {[string match amend* $commit_type]} {
 563                $ui_comm delete 0.0 end
 564                $ui_comm edit reset
 565                $ui_comm edit modified false
 566                rescan {set ui_status_value "Checked out branch '$current_branch'."}
 567        } else {
 568                repository_state commit_type HEAD MERGE_HEAD
 569                set PARENT $HEAD
 570                set ui_status_value "Checked out branch '$current_branch'."
 571        }
 572}