-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: gitmirror v0.1.0, now with concurrency!
- Loading branch information
1 parent
679ef63
commit 12c14d9
Showing
17 changed files
with
357 additions
and
209 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.