git-gui / lib / branch.tclon commit Merge branch 'maint' of git://repo.or.cz/git-gui into maint (c288a2f)
   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        if {$all_heads ne {}} {
 205                radiobutton $w.from.head_r \
 206                        -text {Local Branch:} \
 207                        -value head \
 208                        -variable create_branch_revtype
 209                eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
 210                grid $w.from.head_r $w.from.head_m -sticky w
 211        }
 212        set all_trackings [all_tracking_branches]
 213        if {$all_trackings ne {}} {
 214                set create_branch_trackinghead [lindex $all_trackings 0]
 215                radiobutton $w.from.tracking_r \
 216                        -text {Tracking Branch:} \
 217                        -value tracking \
 218                        -variable create_branch_revtype
 219                eval tk_optionMenu $w.from.tracking_m \
 220                        create_branch_trackinghead \
 221                        $all_trackings
 222                grid $w.from.tracking_r $w.from.tracking_m -sticky w
 223        }
 224        set all_tags [load_all_tags]
 225        if {$all_tags ne {}} {
 226                set create_branch_tag [lindex $all_tags 0]
 227                radiobutton $w.from.tag_r \
 228                        -text {Tag:} \
 229                        -value tag \
 230                        -variable create_branch_revtype
 231                eval tk_optionMenu $w.from.tag_m create_branch_tag $all_tags
 232                grid $w.from.tag_r $w.from.tag_m -sticky w
 233        }
 234        radiobutton $w.from.exp_r \
 235                -text {Revision Expression:} \
 236                -value expression \
 237                -variable create_branch_revtype
 238        entry $w.from.exp_t \
 239                -borderwidth 1 \
 240                -relief sunken \
 241                -width 50 \
 242                -textvariable create_branch_revexp \
 243                -validate key \
 244                -validatecommand {
 245                        if {%d == 1 && [regexp {\s} %S]} {return 0}
 246                        if {%d == 1 && [string length %S] > 0} {
 247                                set create_branch_revtype expression
 248                        }
 249                        return 1
 250                }
 251        grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
 252        grid columnconfigure $w.from 1 -weight 1
 253        pack $w.from -anchor nw -fill x -pady 5 -padx 5
 254
 255        labelframe $w.postActions -text {Post Creation Actions}
 256        checkbutton $w.postActions.checkout \
 257                -text {Checkout after creation} \
 258                -variable create_branch_checkout
 259        pack $w.postActions.checkout -anchor nw
 260        pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
 261
 262        set create_branch_checkout 1
 263        set create_branch_head $current_branch
 264        set create_branch_revtype head
 265        set create_branch_name $repo_config(gui.newbranchtemplate)
 266        set create_branch_revexp {}
 267
 268        bind $w <Visibility> "
 269                grab $w
 270                $w.desc.name_t icursor end
 271                focus $w.desc.name_t
 272        "
 273        bind $w <Key-Escape> "destroy $w"
 274        bind $w <Key-Return> "do_create_branch_action $w;break"
 275        wm title $w "[appname] ([reponame]): Create Branch"
 276        tkwait window $w
 277}
 278
 279proc do_delete_branch_action {w} {
 280        global all_heads
 281        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
 282
 283        set check_rev {}
 284        switch -- $delete_branch_checktype {
 285        head {set check_rev $delete_branch_head}
 286        tracking {set check_rev $delete_branch_trackinghead}
 287        always {set check_rev {:none}}
 288        }
 289        if {$check_rev eq {:none}} {
 290                set check_cmt {}
 291        } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
 292                tk_messageBox \
 293                        -icon error \
 294                        -type ok \
 295                        -title [wm title $w] \
 296                        -parent $w \
 297                        -message "Invalid check revision: $check_rev"
 298                return
 299        }
 300
 301        set to_delete [list]
 302        set not_merged [list]
 303        foreach i [$w.list.l curselection] {
 304                set b [$w.list.l get $i]
 305                if {[catch {set o [git rev-parse --verify $b]}]} continue
 306                if {$check_cmt ne {}} {
 307                        if {$b eq $check_rev} continue
 308                        if {[catch {set m [git merge-base $o $check_cmt]}]} continue
 309                        if {$o ne $m} {
 310                                lappend not_merged $b
 311                                continue
 312                        }
 313                }
 314                lappend to_delete [list $b $o]
 315        }
 316        if {$not_merged ne {}} {
 317                set msg "The following branches are not completely merged into $check_rev:
 318
 319 - [join $not_merged "\n - "]"
 320                tk_messageBox \
 321                        -icon info \
 322                        -type ok \
 323                        -title [wm title $w] \
 324                        -parent $w \
 325                        -message $msg
 326        }
 327        if {$to_delete eq {}} return
 328        if {$delete_branch_checktype eq {always}} {
 329                set msg {Recovering deleted branches is difficult.
 330
 331Delete the selected branches?}
 332                if {[tk_messageBox \
 333                        -icon warning \
 334                        -type yesno \
 335                        -title [wm title $w] \
 336                        -parent $w \
 337                        -message $msg] ne yes} {
 338                        return
 339                }
 340        }
 341
 342        set failed {}
 343        foreach i $to_delete {
 344                set b [lindex $i 0]
 345                set o [lindex $i 1]
 346                if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
 347                        append failed " - $b: $err\n"
 348                } else {
 349                        set x [lsearch -sorted -exact $all_heads $b]
 350                        if {$x >= 0} {
 351                                set all_heads [lreplace $all_heads $x $x]
 352                        }
 353                }
 354        }
 355
 356        if {$failed ne {}} {
 357                tk_messageBox \
 358                        -icon error \
 359                        -type ok \
 360                        -title [wm title $w] \
 361                        -parent $w \
 362                        -message "Failed to delete branches:\n$failed"
 363        }
 364
 365        set all_heads [lsort $all_heads]
 366        populate_branch_menu
 367        destroy $w
 368}
 369
 370proc do_delete_branch {} {
 371        global all_heads tracking_branches current_branch
 372        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
 373
 374        set w .branch_editor
 375        toplevel $w
 376        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
 377
 378        label $w.header -text {Delete Local Branch} \
 379                -font font_uibold
 380        pack $w.header -side top -fill x
 381
 382        frame $w.buttons
 383        button $w.buttons.create -text Delete \
 384                -command [list do_delete_branch_action $w]
 385        pack $w.buttons.create -side right
 386        button $w.buttons.cancel -text {Cancel} \
 387                -command [list destroy $w]
 388        pack $w.buttons.cancel -side right -padx 5
 389        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 390
 391        labelframe $w.list -text {Local Branches}
 392        listbox $w.list.l \
 393                -height 10 \
 394                -width 70 \
 395                -selectmode extended \
 396                -yscrollcommand [list $w.list.sby set]
 397        foreach h $all_heads {
 398                if {$h ne $current_branch} {
 399                        $w.list.l insert end $h
 400                }
 401        }
 402        scrollbar $w.list.sby -command [list $w.list.l yview]
 403        pack $w.list.sby -side right -fill y
 404        pack $w.list.l -side left -fill both -expand 1
 405        pack $w.list -fill both -expand 1 -pady 5 -padx 5
 406
 407        labelframe $w.validate -text {Delete Only If}
 408        radiobutton $w.validate.head_r \
 409                -text {Merged Into Local Branch:} \
 410                -value head \
 411                -variable delete_branch_checktype
 412        eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
 413        grid $w.validate.head_r $w.validate.head_m -sticky w
 414        set all_trackings [all_tracking_branches]
 415        if {$all_trackings ne {}} {
 416                set delete_branch_trackinghead [lindex $all_trackings 0]
 417                radiobutton $w.validate.tracking_r \
 418                        -text {Merged Into Tracking Branch:} \
 419                        -value tracking \
 420                        -variable delete_branch_checktype
 421                eval tk_optionMenu $w.validate.tracking_m \
 422                        delete_branch_trackinghead \
 423                        $all_trackings
 424                grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
 425        }
 426        radiobutton $w.validate.always_r \
 427                -text {Always (Do not perform merge checks)} \
 428                -value always \
 429                -variable delete_branch_checktype
 430        grid $w.validate.always_r -columnspan 2 -sticky w
 431        grid columnconfigure $w.validate 1 -weight 1
 432        pack $w.validate -anchor nw -fill x -pady 5 -padx 5
 433
 434        set delete_branch_head $current_branch
 435        set delete_branch_checktype head
 436
 437        bind $w <Visibility> "grab $w; focus $w"
 438        bind $w <Key-Escape> "destroy $w"
 439        wm title $w "[appname] ([reponame]): Delete Branch"
 440        tkwait window $w
 441}
 442
 443proc switch_branch {new_branch} {
 444        global HEAD commit_type current_branch repo_config
 445
 446        if {![lock_index switch]} return
 447
 448        # -- Our in memory state should match the repository.
 449        #
 450        repository_state curType curHEAD curMERGE_HEAD
 451        if {[string match amend* $commit_type]
 452                && $curType eq {normal}
 453                && $curHEAD eq $HEAD} {
 454        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 455                info_popup {Last scanned state does not match repository state.
 456
 457Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
 458
 459The rescan will be automatically started now.
 460}
 461                unlock_index
 462                rescan {set ui_status_value {Ready.}}
 463                return
 464        }
 465
 466        # -- Don't do a pointless switch.
 467        #
 468        if {$current_branch eq $new_branch} {
 469                unlock_index
 470                return
 471        }
 472
 473        if {$repo_config(gui.trustmtime) eq {true}} {
 474                switch_branch_stage2 {} $new_branch
 475        } else {
 476                set ui_status_value {Refreshing file status...}
 477                set cmd [list git update-index]
 478                lappend cmd -q
 479                lappend cmd --unmerged
 480                lappend cmd --ignore-missing
 481                lappend cmd --refresh
 482                set fd_rf [open "| $cmd" r]
 483                fconfigure $fd_rf -blocking 0 -translation binary
 484                fileevent $fd_rf readable \
 485                        [list switch_branch_stage2 $fd_rf $new_branch]
 486        }
 487}
 488
 489proc switch_branch_stage2 {fd_rf new_branch} {
 490        global ui_status_value HEAD
 491
 492        if {$fd_rf ne {}} {
 493                read $fd_rf
 494                if {![eof $fd_rf]} return
 495                close $fd_rf
 496        }
 497
 498        set ui_status_value "Updating working directory to '$new_branch'..."
 499        set cmd [list git read-tree]
 500        lappend cmd -m
 501        lappend cmd -u
 502        lappend cmd --exclude-per-directory=.gitignore
 503        lappend cmd $HEAD
 504        lappend cmd $new_branch
 505        set fd_rt [open "| $cmd" r]
 506        fconfigure $fd_rt -blocking 0 -translation binary
 507        fileevent $fd_rt readable \
 508                [list switch_branch_readtree_wait $fd_rt $new_branch]
 509}
 510
 511proc switch_branch_readtree_wait {fd_rt new_branch} {
 512        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 513        global current_branch
 514        global ui_comm ui_status_value
 515
 516        # -- We never get interesting output on stdout; only stderr.
 517        #
 518        read $fd_rt
 519        fconfigure $fd_rt -blocking 1
 520        if {![eof $fd_rt]} {
 521                fconfigure $fd_rt -blocking 0
 522                return
 523        }
 524
 525        # -- The working directory wasn't in sync with the index and
 526        #    we'd have to overwrite something to make the switch. A
 527        #    merge is required.
 528        #
 529        if {[catch {close $fd_rt} err]} {
 530                regsub {^fatal: } $err {} err
 531                warn_popup "File level merge required.
 532
 533$err
 534
 535Staying on branch '$current_branch'."
 536                set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
 537                unlock_index
 538                return
 539        }
 540
 541        # -- Update the symbolic ref.  Core git doesn't even check for failure
 542        #    here, it Just Works(tm).  If it doesn't we are in some really ugly
 543        #    state that is difficult to recover from within git-gui.
 544        #
 545        if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
 546                error_popup "Failed to set current branch.
 547
 548This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
 549
 550This should not have occurred.  [appname] will now close and give up.
 551
 552$err"
 553                do_quit
 554                return
 555        }
 556
 557        # -- Update our repository state.  If we were previously in amend mode
 558        #    we need to toss the current buffer and do a full rescan to update
 559        #    our file lists.  If we weren't in amend mode our file lists are
 560        #    accurate and we can avoid the rescan.
 561        #
 562        unlock_index
 563        set selected_commit_type new
 564        if {[string match amend* $commit_type]} {
 565                $ui_comm delete 0.0 end
 566                $ui_comm edit reset
 567                $ui_comm edit modified false
 568                rescan {set ui_status_value "Checked out branch '$current_branch'."}
 569        } else {
 570                repository_state commit_type HEAD MERGE_HEAD
 571                set PARENT $HEAD
 572                set ui_status_value "Checked out branch '$current_branch'."
 573        }
 574}