all repos

mugit @ a02c4e8

馃惍 git server that your cow will love
11 files changed, 95 insertions(+), 59 deletions(-)
git: set default branch; dont replay on list of default branches (#7)

* git: deduplicate get or set-default/head logic

* cli: repo new --default

* git: validate branch name
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2026-04-03 12:32:09 +0300
Parent: 3cf8623
M internal/cli/cli.go
路路路
        83
        83
         								Name:  "private",

      
        84
        84
         								Usage: "make the repository private",

      
        85
        85
         							},

      
        
        86
        +							&cli.StringFlag{

      
        
        87
        +								Name:  "default",

      
        
        88
        +								Usage: "set default branch",

      
        
        89
        +							},

      
        86
        90
         						},

      
        87
        91
         					},

      
        88
        92
         					{

      路路路
        102
        106
         						},

      
        103
        107
         					},

      
        104
        108
         					{

      
        105
        
        -						Name:   "set-head",

      
        106
        
        -						Usage:  "switches repo's head to specified branch",

      
        107
        
        -						Action: c.repoSetHeadAction,

      
        
        109
        +						Name:   "set-default",

      
        
        110
        +						Usage:  "switch repo's default branch",

      
        
        111
        +						Action: c.repoDefaultAction,

      
        108
        112
         						Arguments: []cli.Argument{

      
        109
        113
         							&cli.StringArg{Name: "name"},

      
        110
        114
         						},

      
M internal/cli/repo.go
路路路
        59
        59
         		}

      
        60
        60
         	}

      
        61
        61
         

      
        
        62
        +	defaultBranch := cmd.String("default")

      
        
        63
        +	if defaultBranch != "" {

      
        
        64
        +		if err := repo.SetDefaultBranch(defaultBranch); err != nil {

      
        
        65
        +			return fmt.Errorf("failed to set default branch: %w", err)

      
        
        66
        +		}

      
        
        67
        +	}

      
        
        68
        +

      
        62
        69
         	return nil

      
        63
        70
         }

      
        64
        71
         

      路路路
        114
        121
         	return nil

      
        115
        122
         }

      
        116
        123
         

      
        117
        
        -func (c *Cli) repoSetHeadAction(ctx context.Context, cmd *cli.Command) error {

      
        
        124
        +func (c *Cli) repoDefaultAction(ctx context.Context, cmd *cli.Command) error {

      
        118
        125
         	name, err := c.getRepoNameArg(cmd)

      
        119
        126
         	if name == "" {

      
        120
        127
         		return err

      路路路
        126
        133
         	}

      
        127
        134
         

      
        128
        135
         	branch := cmd.Args().Get(0)

      
        129
        
        -	if err = repo.Checkout(branch); err != nil {

      
        
        136
        +	if err = repo.SetDefaultBranch(branch); err != nil {

      
        130
        137
         		return err

      
        131
        138
         	}

      
        132
        139
         

      
M internal/config/config.go
路路路
        31
        31
         type RepoConfig struct {

      
        32
        32
         	Dir     string   `yaml:"dir"`

      
        33
        33
         	Readmes []string `yaml:"readmes"`

      
        34
        
        -	Masters []string `yaml:"masters"`

      
        35
        34
         }

      
        36
        35
         

      
        37
        36
         type SSHConfig struct {

      路路路
        127
        126
         	}

      
        128
        127
         

      
        129
        128
         	// repos

      
        130
        
        -	if len(c.Repo.Masters) == 0 {

      
        131
        
        -		c.Repo.Masters = []string{"master", "main"}

      
        132
        
        -	}

      
        133
        
        -

      
        134
        129
         	if len(c.Repo.Readmes) == 0 {

      
        135
        130
         		c.Repo.Readmes = []string{

      
        136
        131
         			"README.md", "readme.md",

      
M internal/git/external.go
路路路
        3
        3
         import (

      
        4
        4
         	"cmp"

      
        5
        5
         	"context"

      
        
        6
        +	"errors"

      
        6
        7
         	"fmt"

      
        7
        8
         	"io"

      
        8
        9
         	"os/exec"

      路路路
        33
        34
         	cmd.Stdout = cmp.Or(opts.Stdout, io.Discard)

      
        34
        35
         	cmd.Stderr = cmp.Or(opts.Stderr, io.Discard)

      
        35
        36
         	return cmd.Run()

      
        
        37
        +}

      
        
        38
        +

      
        
        39
        +func (g *Repo) runGitCmd(cmd string, args ...string) ([]byte, error) {

      
        
        40
        +	var gitArgs []string

      
        
        41
        +	gitArgs = append(gitArgs, cmd)

      
        
        42
        +	gitArgs = append(gitArgs, args...)

      
        
        43
        +	gitCmd := exec.Command("git", gitArgs...)

      
        
        44
        +	gitCmd.Dir = g.path

      
        
        45
        +	gitCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        
        46
        +	gitCmd.Env = gitEnv

      
        
        47
        +

      
        
        48
        +	out, err := gitCmd.Output()

      
        
        49
        +	if err != nil {

      
        
        50
        +		if err, ok := errors.AsType[*exec.ExitError](err); ok {

      
        
        51
        +			return nil, fmt.Errorf("%w, stderr: %s", err, err.Stderr)

      
        
        52
        +		}

      
        
        53
        +		return nil, err

      
        
        54
        +	}

      
        
        55
        +	return out, nil

      
        36
        56
         }

      
        37
        57
         

      
        38
        58
         func (g *Repo) streamingGitLog(ctx context.Context, extraArgs ...string) (io.ReadCloser, error) {

      
M internal/git/repo.go
路路路
        95
        95
         	return strings.TrimSuffix(name, ".git")

      
        96
        96
         }

      
        97
        97
         

      
        98
        
        -func (g *Repo) Checkout(ref string) error {

      
        99
        
        -	head := plumbing.NewSymbolicReference(plumbing.HEAD,

      
        100
        
        -		plumbing.NewBranchReferenceName(ref))

      
        
        98
        +func (g *Repo) DefaultBranch() (string, error) {

      
        
        99
        +	out, err := g.runGitCmd("rev-parse", "--abbrev-ref", "HEAD")

      
        
        100
        +	if err != nil {

      
        
        101
        +		return "", fmt.Errorf("failed to get default branch: %w", err)

      
        
        102
        +	}

      
        
        103
        +	return strings.TrimSpace(string(out)), nil

      
        
        104
        +}

      
        
        105
        +

      
        
        106
        +func (g *Repo) SetDefaultBranch(branch string) error {

      
        
        107
        +	b := plumbing.NewBranchReferenceName(branch)

      
        
        108
        +	_, err := g.r.Reference(b, true)

      
        
        109
        +	if err != nil {

      
        
        110
        +		return fmt.Errorf("branch %q not found: %w", branch, err)

      
        
        111
        +	}

      
        
        112
        +	head := plumbing.NewSymbolicReference(plumbing.HEAD, b)

      
        101
        113
         	return g.r.Storer.SetReference(head)

      
        102
        114
         }

      
        103
        115
         

      路路路
        222
        234
         func (g *Repo) IsGoMod() bool {

      
        223
        235
         	_, err := g.FileContent("go.mod")

      
        224
        236
         	return err == nil

      
        225
        
        -}

      
        226
        
        -

      
        227
        
        -func (g *Repo) FindMasterBranch(masters []string) (string, error) {

      
        228
        
        -	if g.IsEmpty() {

      
        229
        
        -		return "", ErrEmptyRepo

      
        230
        
        -	}

      
        231
        
        -

      
        232
        
        -	for _, b := range masters {

      
        233
        
        -		if _, err := g.r.ResolveRevision(plumbing.Revision(b)); err == nil {

      
        234
        
        -			return b, nil

      
        235
        
        -		}

      
        236
        
        -	}

      
        237
        
        -	return "", fmt.Errorf("unable to find master branch")

      
        238
        237
         }

      
        239
        238
         

      
        240
        239
         func (g *Repo) Fetch(ctx context.Context) error {

      
M internal/git/repo_test.go
路路路
        216
        216
         	})

      
        217
        217
         }

      
        218
        218
         

      
        219
        
        -func TestRepo_FindMasterBranch(t *testing.T) {

      
        220
        
        -	t.Run("finds master branch", func(t *testing.T) {

      
        
        219
        +func TestRepo_DefaultBranch(t *testing.T) {

      
        
        220
        +	t.Run("works", func(t *testing.T) {

      
        221
        221
         		r := newTestRepo(t)

      
        222
        
        -		r.commitFile("README.md", "test", "init")

      
        223
        
        -		branch, err := r.open().FindMasterBranch([]string{"master", "main"})

      
        
        222
        +		r.commitFile("readme", "test", "init")

      
        224
        223
         

      
        
        224
        +		branch, err := r.open().DefaultBranch()

      
        225
        225
         		is.Equal(t, branch, "master")

      
        226
        226
         		is.Err(t, err, nil)

      
        227
        227
         	})

      
        228
        228
         

      
        229
        
        -	t.Run("finds first matching candidate", func(t *testing.T) {

      
        
        229
        +	t.Run("multiple branches", func(t *testing.T) {

      
        230
        230
         		r := newTestRepo(t)

      
        231
        
        -		hash := r.commitFile("file.txt", "x", "Commit")

      
        232
        
        -		r.createBranch("main", hash)

      
        
        231
        +		r.commitFile("readme", "test", "init")

      
        
        232
        +		m := r.commitFile("main", "test", "init2")

      
        
        233
        +		r.createBranch("develop", m)

      
        233
        234
         

      
        234
        
        -		// master should be found first (master is created by init)

      
        235
        
        -		branch, err := r.open().FindMasterBranch([]string{"master", "main"})

      
        
        235
        +		branch, err := r.open().DefaultBranch()

      
        236
        236
         		is.Equal(t, branch, "master")

      
        237
        237
         		is.Err(t, err, nil)

      
        238
        
        -

      
        239
        
        -		// if main is check first, it should be found

      
        240
        
        -		branch, err = r.open().FindMasterBranch([]string{"main", "master"})

      
        241
        
        -		is.Equal(t, branch, "main")

      
        242
        
        -		is.Err(t, err, nil)

      
        243
        238
         	})

      
        
        239
        +}

      
        244
        240
         

      
        245
        
        -	t.Run("returns error when no match", func(t *testing.T) {

      
        
        241
        +func TestRepo_SetDefaultBranch(t *testing.T) {

      
        
        242
        +	t.Run("works", func(t *testing.T) {

      
        246
        243
         		r := newTestRepo(t)

      
        247
        
        -		r.commitFile("file.txt", "x", "Commit")

      
        248
        
        -		_, err := r.open().FindMasterBranch([]string{"nonexistent"})

      
        249
        
        -		is.Err(t, err, "unable to find master")

      
        
        244
        +		r.commitFile("readme", "test", "init")

      
        
        245
        +

      
        
        246
        +		rr := r.open()

      
        
        247
        +

      
        
        248
        +		branch, err := rr.DefaultBranch()

      
        
        249
        +		is.Equal(t, branch, "master")

      
        
        250
        +		is.Err(t, err, nil)

      
        
        251
        +

      
        
        252
        +		h := r.commitFile("thing", "hello worldie", "new feature")

      
        
        253
        +		r.createBranch("develop", h)

      
        
        254
        +		is.Err(t, rr.SetDefaultBranch("develop"), nil)

      
        
        255
        +

      
        
        256
        +		branch, err = rr.DefaultBranch()

      
        
        257
        +		is.Equal(t, branch, "develop")

      
        
        258
        +		is.Err(t, err, nil)

      
        250
        259
         	})

      
        251
        260
         

      
        252
        
        -	t.Run("returns error for empty repo", func(t *testing.T) {

      
        
        261
        +	t.Run("sets only existent branches", func(t *testing.T) {

      
        253
        262
         		r := newTestRepo(t)

      
        254
        
        -		_, err := r.open().FindMasterBranch([]string{"main"})

      
        255
        
        -		is.Err(t, err, ErrEmptyRepo)

      
        
        263
        +		r.commitFile("readme", "test", "init")

      
        
        264
        +

      
        
        265
        +		rr := r.open()

      
        
        266
        +		err := rr.SetDefaultBranch("tesites")

      
        
        267
        +		is.Err(t, err, `not found:`)

      
        256
        268
         	})

      
        257
        269
         }

      
M internal/handlers/repo.go
路路路
        88
        88
         		return

      
        89
        89
         	}

      
        90
        90
         

      
        91
        
        -	p.Ref, err = repo.FindMasterBranch(h.c.Repo.Masters)

      
        
        91
        +	p.Ref, err = repo.DefaultBranch()

      
        92
        92
         	if err != nil {

      
        93
        93
         		h.write500(w, err)

      
        94
        94
         		return

      路路路
        349
        349
         		return

      
        350
        350
         	}

      
        351
        351
         

      
        352
        
        -	master, err := repo.FindMasterBranch(h.c.Repo.Masters)

      
        
        352
        +	master, err := repo.DefaultBranch()

      
        353
        353
         	if err != nil {

      
        354
        354
         		h.write500(w, err)

      
        355
        355
         		return

      
A testscript/cli-repo-set-default-errors.txtar
路路路
        
        1
        +# missing repo name

      
        
        2
        +! mugit repo set-default

      
        
        3
        +stderr 'no name provided'

      
        
        4
        +

      
        
        5
        +# repo does not exist

      
        
        6
        +! mugit repo set-default nonexistent main

      
        
        7
        +stderr 'failed to open repo'

      
D testscript/cli-repo-set-head-errors.txtar
路路路
        1
        
        -# missing repo name

      
        2
        
        -! mugit repo set-head

      
        3
        
        -stderr 'no name provided'

      
        4
        
        -

      
        5
        
        -# repo does not exist

      
        6
        
        -! mugit repo set-head nonexistent main

      
        7
        
        -stderr 'failed to open repo'

      
M testscript/cli-repo-set-head.txtartestscript/cli-repo-set-default.txtar
路路路
        20
        20
         stdout 'ref: refs/heads/master'

      
        21
        21
         

      
        22
        22
         # change head

      
        23
        
        -mugit repo set-head heady develop

      
        
        23
        +mugit repo set-default heady develop

      
        24
        24
         stderr 'changed repo head repo=heady.git branch=develop' # fix output

      
        25
        25
         

      
        26
        26
         exec cat $REPOS/heady.git/HEAD

      
        27
        27
         stdout 'ref: refs/heads/develop'

      
        28
        28
         

      
        29
        29
         # go back to master

      
        30
        
        -mugit repo set-head heady.git master

      
        
        30
        +mugit repo set-default heady.git master

      
        31
        31
         stderr 'changed repo head repo=heady.git branch=master' # fix output

      
        32
        32
         

      
        33
        33
         exec cat $REPOS/heady.git/HEAD

      
M testscript_test.go
路路路
        68
        68
         		Repo: config.RepoConfig{

      
        69
        69
         			Dir:     reposDir,

      
        70
        70
         			Readmes: []string{"README.md"},

      
        71
        
        -			Masters: []string{"master", "main"},

      
        72
        71
         		},

      
        73
        72
         		SSH:    config.SSHConfig{Enable: true, User: "git"},

      
        74
        73
         		Mirror: config.MirrorConfig{Enable: false},