go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/branch_test.go (about)

     1  // Copyright 2017 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"math/rand"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	"go.fuchsia.dev/jiri/gitutil"
    21  	"go.fuchsia.dev/jiri/jiritest"
    22  	"go.fuchsia.dev/jiri/project"
    23  )
    24  
    25  func setDefaultBranchFlags() {
    26  	branchFlags.deleteFlag = false
    27  	branchFlags.deleteMergedClsFlag = false
    28  	branchFlags.deleteMergedFlag = false
    29  	branchFlags.forceDeleteFlag = false
    30  	branchFlags.listFlag = false
    31  	branchFlags.overrideProjectConfigFlag = false
    32  }
    33  
    34  func createBranchCommits(t *testing.T, fake *jiritest.FakeJiriRoot, localProjects []project.Project) {
    35  	for i, localProject := range localProjects {
    36  		writeFile(t, fake.X, fake.Projects[localProject.Name], "file1"+strconv.Itoa(i), "file1"+strconv.Itoa(i))
    37  	}
    38  }
    39  
    40  func createBranchProjects(t *testing.T, fake *jiritest.FakeJiriRoot, numProjects int) []project.Project {
    41  	localProjects := []project.Project{}
    42  	for i := 0; i < numProjects; i++ {
    43  		name := fmt.Sprintf("project-%d", i)
    44  		path := fmt.Sprintf("path-%d", i)
    45  		if err := fake.CreateRemoteProject(name); err != nil {
    46  			t.Fatal(err)
    47  		}
    48  		p := project.Project{
    49  			Name:   name,
    50  			Path:   filepath.Join(fake.X.Root, path),
    51  			Remote: fake.Projects[name],
    52  		}
    53  		localProjects = append(localProjects, p)
    54  		if err := fake.AddProject(p); err != nil {
    55  			t.Fatal(err)
    56  		}
    57  	}
    58  	createBranchCommits(t, fake, localProjects)
    59  	return localProjects
    60  }
    61  
    62  func TestBranch(t *testing.T) {
    63  	setDefaultBranchFlags()
    64  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
    65  	defer cleanup()
    66  
    67  	// Add projects
    68  	numProjects := 8
    69  	localProjects := createBranchProjects(t, fake, numProjects)
    70  	if err := fake.UpdateUniverse(false); err != nil {
    71  		t.Fatal(err)
    72  	}
    73  
    74  	gitLocals := make([]*gitutil.Git, numProjects)
    75  	for i, localProject := range localProjects {
    76  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
    77  		gitLocals[i] = gitLocal
    78  	}
    79  
    80  	testBranch := "testBranch"
    81  	testBranch2 := "testBranch2"
    82  
    83  	defaultWant := ""
    84  	branchWant := ""
    85  	listWant := ""
    86  	cDir, err := os.Getwd()
    87  	if err != nil {
    88  		t.Fatal(err)
    89  	}
    90  	relativePath := make([]string, numProjects)
    91  	for i, p := range localProjects {
    92  		relativePath[i], err = filepath.Rel(cDir, p.Path)
    93  		if err != nil {
    94  			t.Fatal(err)
    95  		}
    96  	}
    97  	// current branch is not testBranch
    98  	i := 0
    99  	gitLocals[i].CreateBranch(testBranch)
   100  	gitLocals[i].CheckoutBranch("main", localProjects[i].GitSubmodules, false)
   101  	branchWant = fmt.Sprintf("%s%s(%s)\n", branchWant, localProjects[i].Name, relativePath[i])
   102  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   103  	defaultWant = fmt.Sprintf("%sBranch(es): %s, *main\n\n", defaultWant, testBranch)
   104  
   105  	i = 2
   106  	gitLocals[i].CreateBranch(testBranch)
   107  	gitLocals[i].CreateBranch(testBranch2)
   108  	branchWant = fmt.Sprintf("%s%s(%s)\n", branchWant, localProjects[i].Name, relativePath[i])
   109  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   110  	defaultWant = fmt.Sprintf("%sBranch(es): %s, %s\n\n", defaultWant, testBranch, testBranch2)
   111  
   112  	i = 3
   113  	gitLocals[i].CreateBranch(testBranch)
   114  	branchWant = fmt.Sprintf("%s%s(%s)\n", branchWant, localProjects[i].Name, relativePath[i])
   115  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   116  	defaultWant = fmt.Sprintf("%sBranch(es): %s\n\n", defaultWant, testBranch)
   117  
   118  	// current branch is test branch
   119  	i = 1
   120  	gitLocals[i].CreateBranch(testBranch)
   121  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   122  	gitLocals[i].CreateBranch(testBranch2)
   123  	listWant = fmt.Sprintf("%s%s(%s)\n", listWant, localProjects[i].Name, relativePath[i])
   124  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   125  	defaultWant = fmt.Sprintf("%sBranch(es): *%s, %s\n\n", defaultWant, testBranch, testBranch2)
   126  
   127  	i = 6
   128  	gitLocals[i].CreateBranch(testBranch)
   129  	gitLocals[i].CreateBranch("main")
   130  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   131  	listWant = fmt.Sprintf("%s%s(%s)\n", listWant, localProjects[i].Name, relativePath[i])
   132  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   133  	defaultWant = fmt.Sprintf("%sBranch(es): *%s, main\n\n", defaultWant, testBranch)
   134  
   135  	i = 4
   136  	gitLocals[i].CreateBranch(testBranch)
   137  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   138  	gitLocals[i].CreateBranch(testBranch2)
   139  	listWant = fmt.Sprintf("%s%s(%s)\n", listWant, localProjects[i].Name, relativePath[i])
   140  	branchWant = fmt.Sprintf("%s%s", branchWant, listWant)
   141  	defaultWant = fmt.Sprintf("%sProject: %s(%s)\n", defaultWant, localProjects[i].Name, relativePath[i])
   142  	defaultWant = fmt.Sprintf("%sBranch(es): *%s, %s\n\n", defaultWant, testBranch, testBranch2)
   143  
   144  	// Run default
   145  	if got := executeBranch(t, fake); !equalDefaultBranchOut(got, defaultWant) {
   146  		t.Errorf("got %s, want %s", got, defaultWant)
   147  	}
   148  	// Run with branch
   149  	if got := executeBranch(t, fake, testBranch); !equalBranchOut(got, branchWant) {
   150  		t.Errorf("got %s, want %s", got, branchWant)
   151  	}
   152  
   153  	// Run with listFlag
   154  	branchFlags.listFlag = true
   155  	if got := executeBranch(t, fake, testBranch); !equalBranchOut(got, listWant) {
   156  		t.Errorf("got %s, want %s", got, listWant)
   157  	}
   158  }
   159  
   160  func TestDeleteBranchWithProjectConfig(t *testing.T) {
   161  	testDeleteBranchWithProjectConfig(t, false)
   162  	testDeleteBranchWithProjectConfig(t, true)
   163  }
   164  
   165  func testDeleteBranchWithProjectConfig(t *testing.T, override_pc bool) {
   166  	setDefaultBranchFlags()
   167  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   168  	defer cleanup()
   169  
   170  	// Add projects
   171  	numProjects := 4
   172  	localProjects := createBranchProjects(t, fake, numProjects)
   173  	if err := fake.UpdateUniverse(false); err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	gitLocals := make([]*gitutil.Git, numProjects)
   178  	for i, localProject := range localProjects {
   179  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
   180  		gitLocals[i] = gitLocal
   181  	}
   182  
   183  	testBranch := "testBranch"
   184  
   185  	// Test case when new test branch is on HEAD
   186  	i := 0
   187  	gitLocals[i].CreateBranch(testBranch)
   188  	lc := project.LocalConfig{NoUpdate: true}
   189  	project.WriteLocalConfig(fake.X, localProjects[i], lc)
   190  
   191  	// Test when git branch -d fails
   192  	i = 1
   193  	gitLocals[i].CreateBranch(testBranch)
   194  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   195  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile")
   196  	gitLocals[i].CheckoutBranch("main", localProjects[i].GitSubmodules, false)
   197  
   198  	// Test when current branch is test branch
   199  	i = 2
   200  	gitLocals[i].CreateBranch(testBranch)
   201  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   202  
   203  	// project-3 has no test branch
   204  
   205  	projects := make(project.Projects)
   206  	for _, localProject := range localProjects {
   207  		projects[localProject.Key()] = localProject
   208  	}
   209  
   210  	setDefaultBranchFlags()
   211  	branchFlags.deleteFlag = true
   212  	branchFlags.overrideProjectConfigFlag = override_pc
   213  	executeBranch(t, fake, testBranch)
   214  
   215  	states, err := project.GetProjectStates(fake.X, projects, false)
   216  	if err != nil {
   217  		t.Error(err)
   218  	}
   219  
   220  	// test project states
   221  	for i = 0; i < numProjects; i++ {
   222  		localProject := localProjects[i]
   223  		state, _ := states[localProject.Key()]
   224  		branchFound := false
   225  		for _, branch := range state.Branches {
   226  			if branch.Name == testBranch {
   227  				branchFound = true
   228  				break
   229  			}
   230  		}
   231  		if (!override_pc && i == 0) || i == 1 || i == 2 {
   232  			if !branchFound {
   233  				t.Errorf("project %q should contain branch %q", localProject.Name, testBranch)
   234  			}
   235  		} else {
   236  			if branchFound {
   237  				t.Errorf("project %q should not contain branch %q", localProject.Name, testBranch)
   238  			}
   239  
   240  		}
   241  	}
   242  }
   243  
   244  func TestDeleteBranch(t *testing.T) {
   245  	setDefaultBranchFlags()
   246  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   247  	defer cleanup()
   248  
   249  	// Add projects
   250  	numProjects := 4
   251  	localProjects := createBranchProjects(t, fake, numProjects)
   252  	if err := fake.UpdateUniverse(false); err != nil {
   253  		t.Fatal(err)
   254  	}
   255  
   256  	gitLocals := make([]*gitutil.Git, numProjects)
   257  	for i, localProject := range localProjects {
   258  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
   259  		gitLocals[i] = gitLocal
   260  	}
   261  
   262  	testBranch := "testBranch"
   263  
   264  	// Test case when new test branch is on HEAD
   265  	i := 0
   266  	gitLocals[i].CreateBranch(testBranch)
   267  
   268  	// Test when git branch -d fails
   269  	i = 1
   270  	gitLocals[i].CreateBranch(testBranch)
   271  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   272  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile")
   273  	gitLocals[i].CheckoutBranch("main", localProjects[i].GitSubmodules, false)
   274  
   275  	// Test when current branch is test branch
   276  	i = 2
   277  	gitLocals[i].CreateBranch(testBranch)
   278  	gitLocals[i].CheckoutBranch(testBranch, localProjects[i].GitSubmodules, false)
   279  
   280  	// project-3 has no test branch
   281  
   282  	projects := make(project.Projects)
   283  	for _, localProject := range localProjects {
   284  		projects[localProject.Key()] = localProject
   285  	}
   286  
   287  	// Run on default, should not delete any branch
   288  	executeBranch(t, fake, testBranch)
   289  
   290  	states, err := project.GetProjectStates(fake.X, projects, false)
   291  	if err != nil {
   292  		t.Error(err)
   293  	}
   294  
   295  	// test project states
   296  	for i = 0; i < numProjects; i++ {
   297  		localProject := localProjects[i]
   298  		state, _ := states[localProject.Key()]
   299  		branchFound := false
   300  		for _, branch := range state.Branches {
   301  			if branch.Name == testBranch {
   302  				branchFound = true
   303  				break
   304  			}
   305  		}
   306  		if i == 0 || i == 1 || i == 2 {
   307  			if !branchFound {
   308  				t.Errorf("project %q should contain branch %q", localProject.Name, testBranch)
   309  			}
   310  		}
   311  	}
   312  
   313  	setDefaultBranchFlags()
   314  	branchFlags.deleteFlag = true
   315  	executeBranch(t, fake, testBranch)
   316  
   317  	states, err = project.GetProjectStates(fake.X, projects, false)
   318  	if err != nil {
   319  		t.Error(err)
   320  	}
   321  
   322  	// test project states
   323  	for i = 0; i < numProjects; i++ {
   324  		localProject := localProjects[i]
   325  		state, _ := states[localProject.Key()]
   326  		branchFound := false
   327  		for _, branch := range state.Branches {
   328  			if branch.Name == testBranch {
   329  				branchFound = true
   330  				break
   331  			}
   332  		}
   333  		if i == 1 || i == 2 {
   334  			if !branchFound {
   335  				t.Errorf("project %q should contain branch %q", localProject.Name, testBranch)
   336  			}
   337  		} else {
   338  			if branchFound {
   339  
   340  				t.Errorf("project %q should not contain branch %q", localProject.Name, testBranch)
   341  			}
   342  
   343  		}
   344  	}
   345  
   346  	setDefaultBranchFlags()
   347  	branchFlags.forceDeleteFlag = true
   348  	executeBranch(t, fake, testBranch)
   349  
   350  	states, err = project.GetProjectStates(fake.X, projects, false)
   351  	if err != nil {
   352  		t.Error(err)
   353  	}
   354  
   355  	// test project states
   356  	for i = 0; i < numProjects; i++ {
   357  		localProject := localProjects[i]
   358  		state, _ := states[localProject.Key()]
   359  		branchFound := false
   360  		for _, branch := range state.Branches {
   361  			if branch.Name == testBranch {
   362  				branchFound = true
   363  				break
   364  			}
   365  		}
   366  		if i == 2 {
   367  			if !branchFound {
   368  				t.Errorf("project %q should contain branch %q", localProject.Name, testBranch)
   369  			}
   370  		} else {
   371  			if branchFound {
   372  
   373  				t.Errorf("project %q should not contain branch %q", localProject.Name, testBranch)
   374  			}
   375  
   376  		}
   377  	}
   378  }
   379  
   380  var r *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
   381  
   382  func randomString(strlen int) string {
   383  	const chars = "abcde0123456789ABCDE"
   384  	result := make([]byte, strlen)
   385  	for i := range result {
   386  		result[i] = chars[r.Intn(len(chars))]
   387  	}
   388  	return string(result)
   389  }
   390  
   391  func generateChangeIds(n int) []string {
   392  	ids := make([]string, n)
   393  	for i := range ids {
   394  		ids[i] = "I" + randomString(40)
   395  	}
   396  	return ids
   397  }
   398  
   399  func TestDeleteMergedClsBranch(t *testing.T) {
   400  	mergedIds := generateChangeIds(2)
   401  	unmergedIds := generateChangeIds(1)
   402  	localIds := generateChangeIds(1)
   403  	serverMux := http.NewServeMux()
   404  	serverMux.HandleFunc("/changes/", func(rw http.ResponseWriter, r *http.Request) {
   405  		r.ParseForm()
   406  		if id, ok := r.Form["q"]; ok {
   407  			for _, m := range mergedIds {
   408  				if m == id[0] {
   409  					rw.Write([]byte(")]}'\n[{\"submitted\":\"time\"}]"))
   410  					return
   411  				}
   412  			}
   413  			for _, u := range unmergedIds {
   414  				if u == id[0] {
   415  					rw.Write([]byte(fmt.Sprintf(")]}'\n[{\"change-id\":\"%s\"}]", id[0])))
   416  					return
   417  				}
   418  			}
   419  		}
   420  		rw.Write([]byte(")]}'\n[]"))
   421  	})
   422  	serverMux.HandleFunc("/tools/hooks/commit-msg", func(rw http.ResponseWriter, r *http.Request) {
   423  		rw.Write([]byte("#!/bin/sh"))
   424  	})
   425  	server := httptest.NewServer(serverMux)
   426  	defer server.Close()
   427  
   428  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   429  	defer cleanup()
   430  
   431  	// Add projects
   432  	numProjects := 6
   433  	localProjects := createBranchProjects(t, fake, numProjects)
   434  
   435  	m, err := fake.ReadRemoteManifest()
   436  	if err != nil {
   437  		t.Fatal(err)
   438  	}
   439  	ps := []project.Project{}
   440  	for _, p := range m.Projects {
   441  		p.GerritHost = server.URL
   442  		ps = append(ps, p)
   443  	}
   444  	m.Projects = ps
   445  	if err := fake.WriteRemoteManifest(m); err != nil {
   446  		t.Fatal(err)
   447  	}
   448  
   449  	if err := fake.UpdateUniverse(false); err != nil {
   450  		t.Fatal(err)
   451  	}
   452  
   453  	gitLocals := make([]*gitutil.Git, numProjects)
   454  	for i, localProject := range localProjects {
   455  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
   456  		gitLocals[i] = gitLocal
   457  	}
   458  
   459  	branchToDelete1 := "branchToDelete1"
   460  	branchToDelete2 := "branchToDelete2"
   461  	branchNotToDelete := "branchNotToDelete"
   462  
   463  	changeIdPrefix := "Change-Id: "
   464  
   465  	i := 0
   466  	gitLocals[i].CreateBranch(branchToDelete1)
   467  	gitLocals[i].CheckoutBranch(branchToDelete1, localProjects[i].GitSubmodules, false)
   468  	for j := 0; j < 2; j++ {
   469  		writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+mergedIds[j])
   470  	}
   471  	gitLocals[i].CreateBranchWithUpstream(branchToDelete2, "origin/main")
   472  	gitLocals[i].CheckoutBranch(branchToDelete2, localProjects[i].GitSubmodules, false)
   473  	for j := 0; j < 2; j++ {
   474  		writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+mergedIds[j])
   475  	}
   476  
   477  	i = 1
   478  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   479  	gitLocals[i].CheckoutBranch(branchToDelete1, localProjects[i].GitSubmodules, false)
   480  	for j := 0; j < 2; j++ {
   481  		writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+mergedIds[j])
   482  	}
   483  	gitLocals[i].CreateAndCheckoutBranch(branchNotToDelete)
   484  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile"+changeIdPrefix+localIds[0])
   485  
   486  	i = 2
   487  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   488  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   489  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   490  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+unmergedIds[0])
   491  
   492  	// project-3 has no branch
   493  
   494  	// Don't delete current branch with changes
   495  	i = 4
   496  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   497  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   498  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   499  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+mergedIds[0])
   500  	newfile(t, localProjects[i].Path, "uncommitted.go")
   501  
   502  	// Don't delete branch when it has local commits
   503  	i = 5
   504  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   505  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   506  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+localIds[0])
   507  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile\n"+changeIdPrefix+mergedIds[0])
   508  
   509  	projects := make(project.Projects)
   510  	for _, localProject := range localProjects {
   511  		projects[localProject.Key()] = localProject
   512  	}
   513  
   514  	oldstates, err := project.GetProjectStates(fake.X, projects, false)
   515  	if err != nil {
   516  		t.Error(err)
   517  	}
   518  
   519  	setDefaultBranchFlags()
   520  	branchFlags.deleteMergedClsFlag = true
   521  	got := executeBranch(t, fake)
   522  	fmt.Println(got)
   523  
   524  	newstates, err := project.GetProjectStates(fake.X, projects, false)
   525  	if err != nil {
   526  		t.Error(err)
   527  	}
   528  
   529  	// test project states
   530  	for i = 0; i < numProjects; i++ {
   531  		localProject := localProjects[i]
   532  		oldstate, _ := oldstates[localProject.Key()]
   533  		newstate, _ := newstates[localProject.Key()]
   534  		newBranchMap := make(map[string]bool)
   535  		for _, newb := range newstate.Branches {
   536  			newBranchMap[newb.Name] = true
   537  		}
   538  		for _, oldb := range oldstate.Branches {
   539  			if oldb.Name == branchNotToDelete && !newBranchMap[oldb.Name] {
   540  				t.Errorf("project %q should contain branch %q", localProject.Name, oldb.Name)
   541  			} else if strings.HasPrefix(oldb.Name, "branchToDelete") && newBranchMap[oldb.Name] {
   542  				t.Errorf("project %q should not contain branch %q", localProject.Name, oldb.Name)
   543  			}
   544  		}
   545  	}
   546  }
   547  
   548  func TestDeleteMergedBranch(t *testing.T) {
   549  	testDeleteMergedBranch(t, false)
   550  	testDeleteMergedBranch(t, true)
   551  }
   552  
   553  func testDeleteMergedBranch(t *testing.T, override_pc bool) {
   554  	setDefaultBranchFlags()
   555  	fake, cleanup := jiritest.NewFakeJiriRoot(t)
   556  	defer cleanup()
   557  
   558  	// Add projects
   559  	numProjects := 7
   560  	localProjects := createBranchProjects(t, fake, numProjects)
   561  	if err := fake.UpdateUniverse(false); err != nil {
   562  		t.Fatal(err)
   563  	}
   564  
   565  	gitLocals := make([]*gitutil.Git, numProjects)
   566  	for i, localProject := range localProjects {
   567  		gitLocal := gitutil.New(fake.X, gitutil.UserNameOpt("John Doe"), gitutil.UserEmailOpt("john.doe@example.com"), gitutil.RootDirOpt(localProject.Path))
   568  		gitLocals[i] = gitLocal
   569  	}
   570  
   571  	branchToDelete1 := "branchToDelete1"
   572  	branchToDelete2 := "branchToDelete2"
   573  	branchNotToDelete := "branchNotToDelete"
   574  
   575  	i := 0
   576  	gitLocals[i].CreateBranch(branchToDelete1)
   577  	gitLocals[i].CreateBranchWithUpstream(branchToDelete2, "origin/main")
   578  
   579  	i = 1
   580  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   581  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   582  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile")
   583  
   584  	i = 2
   585  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   586  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   587  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   588  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile")
   589  
   590  	// project-3 has no branch
   591  
   592  	i = 4
   593  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   594  	gitLocals[i].CreateBranch(branchToDelete2)
   595  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   596  	writeFile(t, fake.X, localProjects[i].Path, "extrafile", "extrafile")
   597  	lc := project.LocalConfig{NoUpdate: true}
   598  	project.WriteLocalConfig(fake.X, localProjects[i], lc)
   599  	localProjects[i].LocalConfig = lc
   600  
   601  	// Don't delete current branch with changes
   602  	i = 5
   603  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   604  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   605  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   606  	newfile(t, localProjects[i].Path, "uncommitted.go")
   607  
   608  	// Don't delete current branch with changes
   609  	i = 6
   610  	gitLocals[i].CreateBranch(branchToDelete1)
   611  	gitLocals[i].CreateBranch(branchNotToDelete)
   612  	gitLocals[i].CheckoutBranch(branchNotToDelete, localProjects[i].GitSubmodules, false)
   613  	newfile(t, localProjects[i].Path, "uncommitted.go")
   614  
   615  	projects := make(project.Projects)
   616  	for _, localProject := range localProjects {
   617  		projects[localProject.Key()] = localProject
   618  	}
   619  
   620  	oldstates, err := project.GetProjectStates(fake.X, projects, false)
   621  	if err != nil {
   622  		t.Error(err)
   623  	}
   624  
   625  	setDefaultBranchFlags()
   626  	branchFlags.deleteMergedFlag = true
   627  	branchFlags.overrideProjectConfigFlag = override_pc
   628  	executeBranch(t, fake)
   629  
   630  	newstates, err := project.GetProjectStates(fake.X, projects, false)
   631  	if err != nil {
   632  		t.Error(err)
   633  	}
   634  
   635  	// test project states
   636  	for i = 0; i < numProjects; i++ {
   637  		localProject := localProjects[i]
   638  		dontdelete := localProject.LocalConfig.NoUpdate && !override_pc
   639  		oldstate, _ := oldstates[localProject.Key()]
   640  		newstate, _ := newstates[localProject.Key()]
   641  		newBranchMap := make(map[string]bool)
   642  		for _, newb := range newstate.Branches {
   643  			newBranchMap[newb.Name] = true
   644  		}
   645  		for _, oldb := range oldstate.Branches {
   646  			if (dontdelete || oldb.Name == branchNotToDelete) && !newBranchMap[oldb.Name] {
   647  				t.Errorf("project %q should contain branch %q", localProject.Name, oldb.Name)
   648  			} else if !dontdelete && strings.HasPrefix(oldb.Name, "branchToDelete") && newBranchMap[oldb.Name] {
   649  				t.Errorf("project %q should not contain branch %q", localProject.Name, oldb.Name)
   650  			}
   651  		}
   652  	}
   653  
   654  	// Test that if <branch> is passed only that branch is deleted
   655  	i = 0
   656  	gitLocals[i].CreateBranch(branchToDelete1)
   657  	gitLocals[i].DeleteBranch(branchNotToDelete, gitutil.ForceOpt(true))
   658  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   659  
   660  	i = 1
   661  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   662  	gitLocals[i].DeleteBranch(branchNotToDelete, gitutil.ForceOpt(true))
   663  	gitLocals[i].CreateBranchWithUpstream(branchNotToDelete, "origin/main")
   664  
   665  	i = 2
   666  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   667  	gitLocals[i].DeleteBranch(branchNotToDelete, gitutil.ForceOpt(true))
   668  	gitLocals[i].CreateBranch(branchNotToDelete)
   669  
   670  	i = 3
   671  	gitLocals[i].CreateBranch(branchToDelete1)
   672  	gitLocals[i].DeleteBranch(branchNotToDelete, gitutil.ForceOpt(true))
   673  	gitLocals[i].CreateBranch(branchNotToDelete)
   674  
   675  	i = 4
   676  	gitLocals[i].CreateBranchWithUpstream(branchToDelete1, "origin/main")
   677  	gitLocals[i].DeleteBranch(branchNotToDelete, gitutil.ForceOpt(true))
   678  	gitLocals[i].CreateBranch(branchNotToDelete)
   679  	lc = project.LocalConfig{NoUpdate: true}
   680  	project.WriteLocalConfig(fake.X, localProjects[i], lc)
   681  	localProjects[i].LocalConfig = lc
   682  
   683  	setDefaultBranchFlags()
   684  	branchFlags.deleteMergedFlag = true
   685  	branchFlags.overrideProjectConfigFlag = override_pc
   686  	executeBranch(t, fake, branchToDelete1)
   687  
   688  	newstates, err = project.GetProjectStates(fake.X, projects, false)
   689  	if err != nil {
   690  		t.Error(err)
   691  	}
   692  
   693  	// test project states
   694  	for i = 0; i <= 4; i++ {
   695  		localProject := localProjects[i]
   696  		dontdelete := localProject.LocalConfig.NoUpdate && !override_pc
   697  		newstate, _ := newstates[localProject.Key()]
   698  		newBranchMap := make(map[string]bool)
   699  		for _, newb := range newstate.Branches {
   700  			newBranchMap[newb.Name] = true
   701  		}
   702  
   703  		if !newBranchMap[branchNotToDelete] {
   704  			t.Errorf("project %q should contain branch %q", localProject.Name, branchNotToDelete)
   705  		}
   706  
   707  		if dontdelete && !newBranchMap[branchToDelete1] {
   708  			t.Errorf("project %q should contain branch %q", localProject.Name, branchToDelete1)
   709  		}
   710  
   711  		if !dontdelete && newBranchMap[branchToDelete1] {
   712  			t.Errorf("project %q should not contain branch %q", localProject.Name, branchToDelete1)
   713  		}
   714  	}
   715  
   716  }
   717  
   718  func equalBranchOut(first, second string) bool {
   719  	second = strings.TrimSpace(second)
   720  	firstStrings := strings.Split(first, "\n")
   721  	secondStrings := strings.Split(second, "\n")
   722  	if len(firstStrings) != len(secondStrings) {
   723  		return false
   724  	}
   725  	sort.Strings(firstStrings)
   726  	sort.Strings(secondStrings)
   727  	for i, first := range firstStrings {
   728  		if first != secondStrings[i] {
   729  			return false
   730  		}
   731  	}
   732  	return true
   733  }
   734  
   735  func equalDefaultBranchOut(first, second string) bool {
   736  	second = strings.TrimSpace(second)
   737  	firstStrings := strings.Split(first, "\n\n")
   738  	secondStrings := strings.Split(second, "\n\n")
   739  	if len(firstStrings) != len(secondStrings) {
   740  		return false
   741  	}
   742  	sort.Strings(firstStrings)
   743  	sort.Strings(secondStrings)
   744  	for i, first := range firstStrings {
   745  		if first != secondStrings[i] {
   746  			return false
   747  		}
   748  	}
   749  	return true
   750  }
   751  
   752  func executeBranch(t *testing.T, fake *jiritest.FakeJiriRoot, args ...string) string {
   753  	stderr := ""
   754  	runCmd := func() {
   755  		if err := runBranch(fake.X, args); err != nil {
   756  			stderr = err.Error()
   757  		}
   758  	}
   759  	stdout, _, err := runfunc(runCmd)
   760  	if err != nil {
   761  		t.Fatal(err)
   762  	}
   763  	return strings.TrimSpace(strings.Join([]string{stdout, stderr}, " "))
   764  }