Skip to content

Commit

Permalink
feat: gitmirror v0.1.0, now with concurrency!
Browse files Browse the repository at this point in the history
  • Loading branch information
telemachus committed Oct 19, 2024
1 parent 679ef63 commit 12c14d9
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 209 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: gitmirror test
on:
push:
pull_request:

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
go: ['1.22', '1.23']
name: humane test (using go ${{ matrix.go }} on ${{ matrix.os }})
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- run: make test
89 changes: 89 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
linters-settings:
copyloopvar:
check-alias: true
depguard:
rules:
main:
files:
- $all
deny:
- pkg: reflect
desc: "avoid reflect"
test:
files:
- $all
deny:
- pkg: reflect
desc: "avoid reflect"
errcheck:
check-type-assertions: true
check-blank: true
exclude-functions:
- fmt.Printf
- fmt.Println
- fmt.Fprintf
- fmt.Fprintln
exhaustive:
default-signifies-exhaustive: true
goconst:
min-len: 2
min-occurrences: 3
gocritic:
disabled-checks:
- hugeParam
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
govet:
enable-all: true
shadow:
strict: true
nolintlint:
require-explanation: true
require-specific: true

linters:
disable-all: true
enable:
- bodyclose
- copyloopvar
- cyclop
- depguard
- dogsled
- dupl
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- goconst
- gocritic
- gosec
- gosimple
- govet
- ineffassign
- ireturn
- maintidx
- misspell
- nolintlint
- nakedret
- prealloc
- predeclared
- staticcheck
- stylecheck
- thelper
- typecheck
- unconvert
- unparam
- unused
- whitespace

run:
issues-exit-code: 1

issues:
exclude-dirs:
- internal
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 3-Clause License

Copyright (c) 2021, Peter Aronoff
Copyright (c) 2024, Peter Aronoff
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
32 changes: 15 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
.DEFAULT_GOAL := build
.DEFAULT_GOAL := test

fmt:
go fmt ./...
golangci-lint run --disable-all --no-config -Egofmt --fix
golangci-lint run --disable-all --no-config -Egofumpt --fix

errcheck: fmt
errcheck ./...
lint: fmt
golangci-lint run

staticcheck: errcheck
staticcheck ./...
test:
go test -shuffle on github.com/telemachus/gitmirror/cli

testv:
go test -shuffle on -v github.com/telemachus/gitmirror/cli

vet: staticcheck
go vet ./...
testr:
go test -race -shuffle on github.com/telemachus/gitmirror/cli

build: vet
build: lint testr
go build .

install: build
go install .

test:
go test -shuffle on ./...

testv:
go test -shuffle on -v ./...

clean:
$(RM) git-backup
go clean -i -r -cache

.PHONY: fmt errcheck staticcheck vet build install test testv clean
.PHONY: fmt lint build install test testv testr clean
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# gitmirror

A command line tool to mirror git repositories.

(c) 2024 Peter Aronoff. BSD 3-Clause license; see [LICENSE.txt][license] for
details.

[license]: /LICENSE.txt
150 changes: 96 additions & 54 deletions cli/app.go
Original file line number Diff line number Diff line change
@@ -1,96 +1,138 @@
package cli

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"slices"
"strings"

"github.com/pelletier/go-toml/v2"
)

// App stores information about the application's state.
type App struct {
Err error
ErrMsg string
ExitValue int
Info string
HomeDir string
ExitValue int
HelpWanted bool
QuietWanted bool
VersionWanted bool
}

// NewApp returns a new App pointer.
func NewApp() *App {
return &App{ExitValue: exitSuccess}
}

// ParseFlags handles flags and options in my finicky way.
func (app *App) ParseFlags(args []string) (string, bool) {
flags := flag.NewFlagSet("gitmirror", flag.ContinueOnError)
flags.SetOutput(io.Discard)
var configFile string
var configIsDefault bool
flags.BoolVar(&app.HelpWanted, "help", false, "")
flags.BoolVar(&app.HelpWanted, "h", false, "")
flags.BoolVar(&app.QuietWanted, "quiet", false, "")
flags.BoolVar(&app.QuietWanted, "q", false, "")
flags.BoolVar(&app.VersionWanted, "version", false, "")
flags.BoolVar(&app.VersionWanted, "v", false, "")
flags.StringVar(&configFile, "config", "", "")
flags.StringVar(&configFile, "c", "", "")
err := flags.Parse(args)
switch {
// This must precede all other checks.
case err != nil:
fmt.Fprintf(os.Stderr, "%s: %s\n%s\n", appName, err, appUsage)
app.ExitValue = exitFailure
case app.HelpWanted:
fmt.Println(appUsage)
case app.VersionWanted:
fmt.Printf("%s: %s\n", appName, appVersion)
case configFile == "":
configFile = defaultConfig
configIsDefault = true
}
return configFile, configIsDefault
}

// Repo stores information about a git repository.
type Repo struct {
Dir string
Remote string
}

type Wanted struct {
Repos []*Repo
// NoOp determines whether an App should bail out.
func (app *App) NoOp() bool {
return app.ExitValue != exitSuccess || app.HelpWanted || app.VersionWanted
}

func (app *App) Unmarshal(configFile string, isDefault bool) *Wanted {
if app.Err != nil {
// Unmarshal reads a configuration file and returns a slice of Repo.
func (app *App) Unmarshal(configFile string, configIsDefault bool) []Repo {
if app.NoOp() {
return nil
}

if isDefault {
var usr *user.User
usr, app.Err = user.Current()
if app.Err != nil {
app.Err = fmt.Errorf("%s: %w", appName, app.Err)
if configIsDefault {
usr, err := user.Current()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", appName, err)
app.ExitValue = exitFailure
return nil
}
configFile = fmt.Sprintf("%s%s%s", usr.HomeDir, string(os.PathSeparator), configFile)
app.HomeDir = usr.HomeDir
configFile = filepath.Join(app.HomeDir, configFile)
}

var blob []byte
blob, app.Err = os.ReadFile(configFile)
if app.Err != nil {
app.Err = fmt.Errorf("%s: %w", appName, app.Err)
blob, err := os.ReadFile(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", appName, err)
app.ExitValue = exitFailure
return nil
}

var wanted Wanted
app.Err = toml.Unmarshal(blob, &wanted)
if app.Err != nil {
repos := make([]Repo, 0, 20)
err = json.Unmarshal(blob, &repos)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", appName, err)
app.ExitValue = exitFailure
return nil
}
return &wanted
// We cannot mirror a repository without a local directory and a remote.
return slices.DeleteFunc(repos, func(repo Repo) bool {
return repo.Dir == "" || repo.Remote == ""
})
}

func (app *App) MirrorRepos(wanted *Wanted) {
if app.Err != nil || wanted.Repos == nil {
// Mirror runs git push --mirror on a group of repositories and displays the
// result of the mirror operation.
func (app *App) Mirror(repos []Repo) {
if app.NoOp() || len(repos) == 0 {
return
}

for _, repo := range wanted.Repos {
if repo == nil || repo.Dir == "" {
continue
}
args := []string{"push", "--mirror", repo.Remote}
cmd := exec.Command("git", args...)
cmd.Dir = repo.Dir
cmdString := fmt.Sprintf("`git %s` (in %s)", strings.Join(args, " "), cmd.Dir)
app.Err = cmd.Run()
if app.Err != nil {
app.Err = fmt.Errorf("%s: problem with %s: %w", appName, cmdString, app.Err)
return
}
fmt.Printf("%s: %s\n", appName, cmdString)
ch := make(chan Publisher)
for _, repo := range repos {
go app.mirror(repo, ch)
}
}

func (app *App) DisplayInfo() {
// Do I need—or want—the or condition here?
if app.Info == "" || app.Err != nil {
return
for range repos {
result := <-ch
result.Publish(app.QuietWanted)
}
fmt.Println(app.Info)
}

func (app *App) DisplayError() {
if app.Err == nil {
func (app *App) mirror(repo Repo, ch chan<- Publisher) {
args := []string{"push", "--mirror", repo.Remote}
cmd := exec.Command("git", args...)
cmd.Dir = repo.Dir
cmdString := fmt.Sprintf(
"git %s (in %s)",
strings.Join(args, " "),
strings.Replace(cmd.Dir, app.HomeDir, "~", 1),
)
err := cmd.Run()
if err != nil {
app.ExitValue = exitFailure
ch <- Failure{msg: fmt.Sprintf("%s: %s: %s", appName, cmdString, err)}
return
}
fmt.Fprintln(os.Stderr, app.Err)
ch <- Success{msg: fmt.Sprintf("%s: %s", appName, cmdString)}
}
16 changes: 0 additions & 16 deletions cli/globals.go

This file was deleted.

Loading

0 comments on commit 12c14d9

Please sign in to comment.