From 4100f4261812418a4d3e9b75ab2ca1caecfc9bfc Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 16:22:47 -0600 Subject: [PATCH 01/23] Initial stab --- .github/workflows/release.yml | 28 --- .gitignore | 2 + .goreleaser.yml | 29 --- README.md | 2 +- cmd/commitstat/main.go | 124 ----------- commitstat/cli.py | 120 +++++++++++ commitstat/core.py | 135 ++++++++++++ commitstat/stats.py | 44 ++++ go.mod | 5 - go.sum | 286 -------------------------- install.sh | 375 ---------------------------------- internal/github/events.go | 45 ---- internal/github/main.go | 107 ---------- internal/parse/main.go | 41 ---- internal/parse/main_test.go | 42 ---- internal/status/main.go | 90 -------- internal/status/main_test.go | 77 ------- internal/version/main.go | 11 - mypy_example.txt | 112 ---------- poetry.lock | 224 ++++++++++++++++++++ pyproject.toml | 23 +++ scripts/format | 3 + scripts/install | 2 +- scripts/pre-commit | 3 +- scripts/run | 2 - scripts/test | 2 - 26 files changed, 555 insertions(+), 1379 deletions(-) delete mode 100644 .github/workflows/release.yml create mode 100644 .gitignore delete mode 100644 .goreleaser.yml delete mode 100644 cmd/commitstat/main.go create mode 100644 commitstat/cli.py create mode 100644 commitstat/core.py create mode 100644 commitstat/stats.py delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 install.sh delete mode 100644 internal/github/events.go delete mode 100644 internal/github/main.go delete mode 100644 internal/parse/main.go delete mode 100644 internal/parse/main_test.go delete mode 100644 internal/status/main.go delete mode 100644 internal/status/main_test.go delete mode 100644 internal/version/main.go delete mode 100644 mypy_example.txt create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100755 scripts/format delete mode 100755 scripts/run delete mode 100755 scripts/test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index cbc4048..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: release - -on: - push: - tags: ["*"] - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..033df5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +__pycache__ diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 23f50c1..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,29 +0,0 @@ -project_name: commitstat -release: - github: - owner: dropseed - name: commitstat -builds: -- goos: - - linux - - darwin - goarch: - - amd64 - - "386" - - arm64 - goarm: - - "6" - main: ./cmd/commitstat - ldflags: -s -w -X github.com/dropseed/commitstat/internal/version.version={{.Version}} -X github.com/dropseed/commitstat/internal/version.commit={{.Commit}} -X github.com/dropseed/commitstat/internal/version.date={{.Date}} - binary: commitstat - env: - - CGO_ENABLED=0 -archives: - - format: tar.gz - name_template: '{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ - .Arm }}{{ end }}' - files: - - LICENSE* - - README* -snapshot: - name_template: SNAPSHOT-{{ .Commit }} diff --git a/README.md b/README.md index 85c8b92..23b5d20 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ There aren't any visualization tools built-in but you can always store artifacts You can install commitstat locally to test your parsing patterns, but commit statuses will only be created when run in CI. ```console -$ curl https://raw.githubusercontent.com/dropseed/commitstat/master/install.sh | bash -s -- -b $HOME/bin +pipx install commitstat ``` ## Options diff --git a/cmd/commitstat/main.go b/cmd/commitstat/main.go deleted file mode 100644 index 0a932e8..0000000 --- a/cmd/commitstat/main.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "strings" - - "github.com/dropseed/commitstat/internal/github" - "github.com/dropseed/commitstat/internal/parse" - "github.com/dropseed/commitstat/internal/status" - "github.com/dropseed/commitstat/internal/version" - "github.com/spf13/cobra" -) - -var verbose bool -var regex string -var statName string -var goal string - -var rootCmd = &cobra.Command{ - Use: "commitstat [file to parse]", - Version: version.WithMeta, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return errors.New("arg should be a path or \"-\" to read from stdin") - } - return nil - }, - Run: func(cmd *cobra.Command, args []string) { - var contents string - - if args[0] == "-" { - bytes, _ := ioutil.ReadAll(os.Stdin) - contents = string(bytes) - } else { - filename := args[0] - fileContent, err := ioutil.ReadFile(filename) - if err != nil { - printErrAndExitFailure(err) - } - contents = string(fileContent) - } - - // Assume stat was given directly by default - stat := strings.TrimSpace(contents) - - if regex != "" { - fmt.Printf("Parsing stat using: %s\n", regex) - parsedStat, err := parse.ParseStatFromString(contents, regex) - if err != nil { - printErrAndExitFailure(err) - } - stat = parsedStat - } - - fmt.Printf("Parsed stat value: %s\n", stat) - - if os.Getenv("GITHUB_ACTIONS") != "" { - if event := os.Getenv("GITHUB_EVENT_NAME"); event != "push" { - println("Stat comparisons are only made for \"push\" events") - return - } - - githubToken := os.Getenv("GITHUB_TOKEN") - if githubToken == "" { - printErrAndExitFailure(errors.New("GITHUB_TOKEN is required to submit stats")) - } - - repo := os.Getenv("GITHUB_REPOSITORY") - if repo == "" { - printErrAndExitFailure(errors.New("GITHUB_REPOSITORY is required")) - } - - pushEvent := github.NewGitHubPushEvent(os.Getenv("GITHUB_EVENT_PATH")) - comparisonRef := pushEvent.GetComparisonRef() - fmt.Printf("Fetching comparision stat from %s\n", comparisonRef) - - comparisonStat, err := github.FetchRefStat(repo, comparisonRef, statName, githubToken) - if err != nil { - printErrAndExitFailure(err) - } - - if comparisonStat != "" { - fmt.Printf("Comparision stat from %s: %s\n", comparisonRef, comparisonStat) - } else { - fmt.Printf("No comparison stat found on %s\n", comparisonRef) - } - - status, err := status.NewStatus(stat, comparisonStat, comparisonRef, goal) - if err != nil { - printErrAndExitFailure(err) - } - - fmt.Printf("%s: %s\n", status.State, status.Description) - - sha := os.Getenv("GITHUB_SHA") - if err := github.SubmitStatus(repo, sha, statName, status, githubToken); err != nil { - printErrAndExitFailure(err) - } - } else { - println("Run in GitHub Actions to create commit statuses") - } - }, -} - -func init() { - rootCmd.Flags().StringVar(®ex, "regex", "", "regex to parse the stat (optional)") - rootCmd.Flags().StringVar(&goal, "goal", "", "goal for the stat (\"increase\" or \"decrease\", optional)") - rootCmd.Flags().StringVar(&statName, "name", "", "name for the stat") - rootCmd.MarkFlagRequired("name") -} - -func printErrAndExitFailure(err error) { - println(err.Error()) - os.Exit(1) -} - -func main() { - if err := rootCmd.Execute(); err != nil { - printErrAndExitFailure(err) - } -} diff --git a/commitstat/cli.py b/commitstat/cli.py new file mode 100644 index 0000000..85e4fc4 --- /dev/null +++ b/commitstat/cli.py @@ -0,0 +1,120 @@ +import subprocess +import click +from .core import Stats + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option("--key", "-k", required=True) +@click.option("--value", "-v", required=True) # value as arg so it's pipeable? +@click.option("--commitish", default="HEAD") +@click.option("--quiet", "-q", is_flag=True) +def save(key, value, commitish, quiet): + Stats().save(key=key, value=value, commitish=commitish, quiet=quiet) + + +@cli.command( + context_settings=dict( + ignore_unknown_options=True, + ) +) +@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option("--default-value", "-d", default="0") +@click.option("--summarize", "-s", is_flag=True) +@click.option("--values-only", is_flag=True) +@click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) +def log(keys, default_value, values_only, summarize, git_log_args): + Stats().log( + keys=keys, + default_value=default_value, + values_only=values_only, + summarize=summarize, + git_log_args=list(git_log_args), + ) + + +@cli.command() +@click.argument("commitish", default="HEAD") +def show(commitish): + try: + Stats().show(commitish) + except subprocess.CalledProcessError as e: + exit(e.returncode) + + +@cli.command() +def push(): + Stats().push() + + +@cli.command() +def fetch(): + Stats().fetch() + + +@cli.command( + context_settings=dict( + ignore_unknown_options=True, + ) +) +@click.option("--key", "-k", required=True) +@click.option("--command", "-c", required=True) +@click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) +def regen(key, command, git_log_args): + current_branch = ( + subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) + .decode("utf-8") + .strip() + ) + + args = ["git", "log", "--format=%H"] + if git_log_args: + args.extend(git_log_args) + + try: + output = subprocess.check_output(args).decode("utf-8") + except subprocess.CalledProcessError as e: + exit(e.returncode) + + commits = output.splitlines() + + if not click.prompt( + f'Regenerate "{key}" stat for {len(commits)} commits?', default=True + ): + exit(1) + + try: + with click.progressbar(commits, show_pos=True, show_eta=True) as items: + for commit in items: + subprocess.check_call( + ["git", "checkout", commit], + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + ) + + value = ( + subprocess.check_output(command, shell=True).decode("utf-8").strip() + ) + Stats().save(key=key, value=value, commitish=commit, quiet=True) + finally: + # Make sure we reset to the ref where we started + subprocess.check_call( + ["git", "checkout", current_branch], + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + ) + + +@cli.command() +@click.option("--key", "-k", default="") +@click.option("--commitish", default="HEAD") +def delete(key, commitish): + Stats().delete(key, commitish) + + +if __name__ == "__main__": + cli() diff --git a/commitstat/core.py b/commitstat/core.py new file mode 100644 index 0000000..50e6d98 --- /dev/null +++ b/commitstat/core.py @@ -0,0 +1,135 @@ +from .stats import CommitStats +import subprocess + + +class Stats: + def __init__(self): + self.stats_ref = "stats" + self.stats_ref_path = "refs/notes/stats" + + def save(self, *, key, value, commitish="HEAD", quiet=False): + stat_line = f"{key}: {value}" + + if existing_stats := self.get(commitish): + edited_stat_lines = [] + had_stat = False + + for line in existing_stats.splitlines(): + if line.startswith(f"{key}:"): + # Update the existing stat + edited_stat_lines.append(stat_line) + had_stat = True + else: + # Keep other existing stats + edited_stat_lines.append(line) + + if not had_stat: + # Append the new stat + edited_stat_lines.append(stat_line) + + message = "\n".join(edited_stat_lines) + else: + message = stat_line + + subprocess.check_call( + [ + "git", + "notes", + "--ref", + self.stats_ref, + "add", + "--force", + "--message", + message, + commitish, + ], + # Don't show the "Overwriting existing notes for commit" + stderr=subprocess.DEVNULL, + ) + + if not quiet: + self.show(commitish) + + def delete(self, key, commitish="HEAD"): + if existing_stats := self.get(commitish): + edited_stat_lines = [] + + for line in existing_stats.splitlines(): + if not line.startswith(f"{key}:"): + edited_stat_lines.append(line) + + message = "\n".join(edited_stat_lines) + + subprocess.check_call( + [ + "git", + "notes", + "--ref", + self.stats_ref, + "add", + "--force", + "--message", + message, + commitish, + ], + # Don't show the "Overwriting existing notes for commit" + stderr=subprocess.DEVNULL, + ) + + def get(self, commitish="HEAD"): + try: + return subprocess.check_output( + ["git", "notes", "--ref", self.stats_ref, "show", commitish], + stderr=subprocess.DEVNULL, + ).decode("utf-8") + except subprocess.CalledProcessError: + return None + + def show(self, commitish="HEAD"): + subprocess.check_call( + ["git", "notes", "--ref", self.stats_ref, "show", commitish] + ) + + def log(self, *, keys, default_value, values_only, summarize, git_log_args=[]): + stats = CommitStats() + + output = subprocess.check_output( + [ + "git", + "log", + f"--show-notes={self.stats_ref_path}", + "--format=COMMIT\n%H\n%N", + *git_log_args, + ] + ).decode("utf-8") + + commit = "" + + for line in output.splitlines(): + if commit is None: + commit = line + stats.append_commit(commit) + continue + + if line == "COMMIT": + commit = None + continue + + if not line.strip(): + continue + + key, value = line.split(":", 1) + + if key in keys or not keys: + # TODO default value may have to be per-key... config file? + stats.add( + commit=commit, key=key, value=value, default_value=default_value + ) + + stats.print(values_only=values_only) + + def push(self): + subprocess.check_call(["git", "push", "origin", self.stats_ref_path]) + + def fetch(self): + subprocess.check_call(["git", "fetch", "origin", self.stats_ref_path]) diff --git a/commitstat/stats.py b/commitstat/stats.py new file mode 100644 index 0000000..5f39512 --- /dev/null +++ b/commitstat/stats.py @@ -0,0 +1,44 @@ +class CommitStats: + def __init__(self): + self.stats = {} # Keep a reference to stats by key + self.commits = [] # Keep an ordered list of commits + + def append_commit(self, commit): + self.commits.append(commit) + + def add(self, *, commit, key, value, default_value): + self.stats.setdefault(key, CommitStat(default_value=default_value)).add( + commit, value + ) + + def print(self, values_only=False, delimiter="\t"): + if not values_only: + print("commit", end=delimiter) + for key in self.stats.keys(): + print(key, end=delimiter) + print() + + for commit in self.commits: + if not values_only: + print(commit, end=delimiter) + + for stat in self.stats.values(): + print(stat.get(commit), end=delimiter) + print() + + +class CommitStat: + """A single stat for multiple commits, stored by commit sha""" + + def __init__(self, default_value=""): + # self.key = key # doesn't need to know this? + self.commit_values = {} + self.default_value = default_value + + def add(self, commit, value): + value = value.strip() # Strip whitespace + # TODO parse units? + self.commit_values[commit] = value + + def get(self, commit): + return self.commit_values.get(commit, self.default_value) diff --git a/go.mod b/go.mod deleted file mode 100644 index 157c75f..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/dropseed/commitstat - -go 1.16 - -require github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum deleted file mode 100644 index a759029..0000000 --- a/go.sum +++ /dev/null @@ -1,286 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/install.sh b/install.sh deleted file mode 100644 index 5b3e577..0000000 --- a/install.sh +++ /dev/null @@ -1,375 +0,0 @@ -#!/bin/sh -set -e -# Code generated by godownloader on 2021-04-29T22:30:14Z. DO NOT EDIT. -# - -usage() { - this=$1 - cat </dev/null -} -echoerr() { - echo "$@" 1>&2 -} -log_prefix() { - echo "$0" -} -_logp=6 -log_set_priority() { - _logp="$1" -} -log_priority() { - if test -z "$1"; then - echo "$_logp" - return - fi - [ "$1" -le "$_logp" ] -} -log_tag() { - case $1 in - 0) echo "emerg" ;; - 1) echo "alert" ;; - 2) echo "crit" ;; - 3) echo "err" ;; - 4) echo "warning" ;; - 5) echo "notice" ;; - 6) echo "info" ;; - 7) echo "debug" ;; - *) echo "$1" ;; - esac -} -log_debug() { - log_priority 7 || return 0 - echoerr "$(log_prefix)" "$(log_tag 7)" "$@" -} -log_info() { - log_priority 6 || return 0 - echoerr "$(log_prefix)" "$(log_tag 6)" "$@" -} -log_err() { - log_priority 3 || return 0 - echoerr "$(log_prefix)" "$(log_tag 3)" "$@" -} -log_crit() { - log_priority 2 || return 0 - echoerr "$(log_prefix)" "$(log_tag 2)" "$@" -} -uname_os() { - os=$(uname -s | tr '[:upper:]' '[:lower:]') - case "$os" in - cygwin_nt*) os="windows" ;; - mingw*) os="windows" ;; - msys_nt*) os="windows" ;; - esac - echo "$os" -} -uname_arch() { - arch=$(uname -m) - case $arch in - x86_64) arch="amd64" ;; - x86) arch="386" ;; - i686) arch="386" ;; - i386) arch="386" ;; - aarch64) arch="arm64" ;; - armv5*) arch="armv5" ;; - armv6*) arch="armv6" ;; - armv7*) arch="armv7" ;; - esac - echo ${arch} -} -uname_os_check() { - os=$(uname_os) - case "$os" in - darwin) return 0 ;; - dragonfly) return 0 ;; - freebsd) return 0 ;; - linux) return 0 ;; - android) return 0 ;; - nacl) return 0 ;; - netbsd) return 0 ;; - openbsd) return 0 ;; - plan9) return 0 ;; - solaris) return 0 ;; - windows) return 0 ;; - esac - log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" - return 1 -} -uname_arch_check() { - arch=$(uname_arch) - case "$arch" in - 386) return 0 ;; - amd64) return 0 ;; - arm64) return 0 ;; - armv5) return 0 ;; - armv6) return 0 ;; - armv7) return 0 ;; - ppc64) return 0 ;; - ppc64le) return 0 ;; - mips) return 0 ;; - mipsle) return 0 ;; - mips64) return 0 ;; - mips64le) return 0 ;; - s390x) return 0 ;; - amd64p32) return 0 ;; - esac - log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" - return 1 -} -untar() { - tarball=$1 - case "${tarball}" in - *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; - *.tar) tar --no-same-owner -xf "${tarball}" ;; - *.zip) unzip "${tarball}" ;; - *) - log_err "untar unknown archive format for ${tarball}" - return 1 - ;; - esac -} -http_download_curl() { - local_file=$1 - source_url=$2 - header=$3 - if [ -z "$header" ]; then - code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") - else - code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") - fi - if [ "$code" != "200" ]; then - log_debug "http_download_curl received HTTP status $code" - return 1 - fi - return 0 -} -http_download_wget() { - local_file=$1 - source_url=$2 - header=$3 - if [ -z "$header" ]; then - wget -q -O "$local_file" "$source_url" - else - wget -q --header "$header" -O "$local_file" "$source_url" - fi -} -http_download() { - log_debug "http_download $2" - if is_command curl; then - http_download_curl "$@" - return - elif is_command wget; then - http_download_wget "$@" - return - fi - log_crit "http_download unable to find wget or curl" - return 1 -} -http_copy() { - tmp=$(mktemp) - http_download "${tmp}" "$1" "$2" || return 1 - body=$(cat "$tmp") - rm -f "${tmp}" - echo "$body" -} -github_release() { - owner_repo=$1 - version=$2 - test -z "$version" && version="latest" - giturl="https://github.com/${owner_repo}/releases/${version}" - json=$(http_copy "$giturl" "Accept:application/json") - test -z "$json" && return 1 - version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') - test -z "$version" && return 1 - echo "$version" -} -hash_sha256() { - TARGET=${1:-/dev/stdin} - if is_command gsha256sum; then - hash=$(gsha256sum "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command sha256sum; then - hash=$(sha256sum "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command shasum; then - hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 - echo "$hash" | cut -d ' ' -f 1 - elif is_command openssl; then - hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 - echo "$hash" | cut -d ' ' -f a - else - log_crit "hash_sha256 unable to find command to compute sha-256 hash" - return 1 - fi -} -hash_sha256_verify() { - TARGET=$1 - checksums=$2 - if [ -z "$checksums" ]; then - log_err "hash_sha256_verify checksum file not specified in arg2" - return 1 - fi - BASENAME=${TARGET##*/} - want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) - if [ -z "$want" ]; then - log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" - return 1 - fi - got=$(hash_sha256 "$TARGET") - if [ "$want" != "$got" ]; then - log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" - return 1 - fi -} -cat /dev/null < prevStatNum { - diff := strings.TrimRight(fmt.Sprintf("%f", statNum-prevStatNum), ".0") - description = fmt.Sprintf("%s - increased by %s%s (%s on %s)", stat, diff, statUnits, comparisonStat, comparisonRef) - } else if statNum == prevStatNum { - description = fmt.Sprintf("%s - same as %s", stat, comparisonRef) - } else { - diff := strings.TrimRight(fmt.Sprintf("%f", prevStatNum-statNum), ".0") - description = fmt.Sprintf("%s - decreased by %s%s (%s on %s)", stat, diff, statUnits, comparisonStat, comparisonRef) - } - - if goal == "decrease" { - if statNum < prevStatNum { - return stateSuccess, description, nil - } else if statNum == prevStatNum { - return stateSuccess, description, nil - } else { - return stateFailure, description, nil - } - } else if goal == "increase" { - if statNum > prevStatNum { - return stateSuccess, description, nil - } else if statNum == prevStatNum { - return stateSuccess, description, nil - } else { - return stateFailure, description, nil - } - } else if goal == "" { - // Always success if there was no stated goal - return stateSuccess, description, nil - } else { - return stateError, "unknown goal", errors.New("unknown goal") - } -} diff --git a/internal/status/main_test.go b/internal/status/main_test.go deleted file mode 100644 index 4aead93..0000000 --- a/internal/status/main_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package status - -import "testing" - -func TestCompareDecreaseSuccess(t *testing.T) { - state, description, err := compareStats("34%", "35%", "master", "decrease") - if err != nil { - t.Error(err) - } - if state != stateSuccess { - t.Error(state) - } - if description != "34% - decreased by 1% (35% on master)" { - t.Error(description) - } -} -func TestCompareDecreaseFail(t *testing.T) { - state, description, err := compareStats("35.56%", "35%", "master", "decrease") - if err != nil { - t.Error(err) - } - if state != stateFailure { - t.Error(state) - } - if description != "35.56% - increased by 0.56% (35% on master)" { - t.Error(description) - } -} - -func TestCompareIncreaseSuccess(t *testing.T) { - state, description, err := compareStats("34mb", "35mb", "master", "increase") - if err != nil { - t.Error(err) - } - if state != stateFailure { - t.Error(state) - } - if description != "34mb - decreased by 1mb (35mb on master)" { - t.Error(description) - } -} -func TestCompareIncreaseFail(t *testing.T) { - state, description, err := compareStats("35.56mb", "35mb", "master", "increase") - if err != nil { - t.Error(err) - } - if state != stateSuccess { - t.Error(state) - } - if description != "35.56mb - increased by 0.56mb (35mb on master)" { - t.Error(description) - } -} -func TestCompareIncreaseEqual(t *testing.T) { - state, description, err := compareStats("35mb", "35mb", "master", "increase") - if err != nil { - t.Error(err) - } - if state != stateSuccess { - t.Error(state) - } - if description != "35mb - same as master" { - t.Error(description) - } -} -func TestCompareDiffUnits(t *testing.T) { - state, description, err := compareStats("35mb", "35gb", "master", "increase") - if err != nil { - t.Error(err) - } - if state != stateError { - t.Error(state) - } - if description != "Stats are not the same units: mb vs gb" { - t.Error(description) - } -} diff --git a/internal/version/main.go b/internal/version/main.go deleted file mode 100644 index 10acaeb..0000000 --- a/internal/version/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package version - -import "fmt" - -var ( - version = "dev" - commit = "none" - date = "unknown" -) - -var WithMeta = fmt.Sprintf("%v\ncommit %v\nbuilt at %v", version, commit, date) diff --git a/mypy_example.txt b/mypy_example.txt deleted file mode 100644 index 7f96628..0000000 --- a/mypy_example.txt +++ /dev/null @@ -1,112 +0,0 @@ -Mypy Type Check Coverage Summary -================================ - -Script: index - -+-------------------------------------+-------------------+----------+ -| Module | Imprecision | Lines | -+-------------------------------------+-------------------+----------+ -| config | 0.00% imprecise | 0 LOC | -| config.loader | 28.57% imprecise | 77 LOC | -| config.nodes | 0.00% imprecise | 0 LOC | -| config.nodes.conditions | 41.67% imprecise | 12 LOC | -| config.nodes.groups | 40.91% imprecise | 44 LOC | -| config.nodes.labels | 46.67% imprecise | 15 LOC | -| config.nodes.notifications | 54.55% imprecise | 33 LOC | -| config.nodes.pullapprove_conditions | 51.35% imprecise | 37 LOC | -| config.nodes.requirements | 41.67% imprecise | 12 LOC | -| config.nodes.reviewers | 35.29% imprecise | 17 LOC | -| config.nodes.reviews | 60.00% imprecise | 35 LOC | -| config.nodes.root | 53.13% imprecise | 32 LOC | -| config.nodes.teams | 41.67% imprecise | 12 LOC | -| config.nodes.users | 41.67% imprecise | 12 LOC | -| config.nodes.version | 8.33% imprecise | 12 LOC | -| context | 0.00% imprecise | 0 LOC | -| context.base | 39.32% imprecise | 117 LOC | -| context.bitbucket | 11.63% imprecise | 43 LOC | -| context.events | 8.00% imprecise | 25 LOC | -| context.functions | 36.41% imprecise | 195 LOC | -| context.github | 13.25% imprecise | 385 LOC | -| context.gitlab | 10.00% imprecise | 30 LOC | -| context.groups | 25.00% imprecise | 96 LOC | -| context.tests | 0.00% imprecise | 0 LOC | -| context.tests.test_gitlab | 50.00% imprecise | 6 LOC | -| context.tests.test_lists | 45.83% imprecise | 24 LOC | -| exceptions | 0.00% imprecise | 8 LOC | -| external | 0.00% imprecise | 0 LOC | -| external.availability | 38.89% imprecise | 54 LOC | -| external.billing | 32.26% imprecise | 124 LOC | -| handlers | 33.33% imprecise | 21 LOC | -| handlers.base | 0.00% imprecise | 2 LOC | -| handlers.base.core | 23.08% imprecise | 13 LOC | -| handlers.base.pull_request | 28.57% imprecise | 301 LOC | -| handlers.bitbucket | 25.00% imprecise | 24 LOC | -| handlers.bitbucket.pullrequest | 32.08% imprecise | 53 LOC | -| handlers.github | 27.78% imprecise | 36 LOC | -| handlers.github.installation | 34.15% imprecise | 41 LOC | -| handlers.github.pull_request | 32.56% imprecise | 43 LOC | -| handlers.github.repo | 32.95% imprecise | 88 LOC | -| handlers.gitlab | 21.74% imprecise | 23 LOC | -| handlers.gitlab.merge_request | 20.45% imprecise | 44 LOC | -| logger | 33.33% imprecise | 54 LOC | -| main | 49.12% imprecise | 57 LOC | -| mode | 9.52% imprecise | 21 LOC | -| models | 0.00% imprecise | 0 LOC | -| models.base | 0.00% imprecise | 3 LOC | -| models.base.api | 32.50% imprecise | 160 LOC | -| models.base.pull_request | 11.83% imprecise | 186 LOC | -| models.base.repo | 26.47% imprecise | 34 LOC | -| models.bitbucket | 0.00% imprecise | 2 LOC | -| models.bitbucket.api | 0.00% imprecise | 9 LOC | -| models.bitbucket.pullrequest | 25.37% imprecise | 201 LOC | -| models.bitbucket.repo | 12.50% imprecise | 64 LOC | -| models.bitbucket.settings | 0.00% imprecise | 11 LOC | -| models.bitbucket.states | 0.00% imprecise | 22 LOC | -| models.events | 29.79% imprecise | 47 LOC | -| models.expressions | 23.68% imprecise | 38 LOC | -| models.github | 0.00% imprecise | 3 LOC | -| models.github.api | 0.00% imprecise | 14 LOC | -| models.github.installation | 32.00% imprecise | 100 LOC | -| models.github.pull_request | 28.42% imprecise | 285 LOC | -| models.github.repo | 27.20% imprecise | 125 LOC | -| models.github.settings | 0.00% imprecise | 9 LOC | -| models.github.states | 0.00% imprecise | 22 LOC | -| models.gitlab | 0.00% imprecise | 2 LOC | -| models.gitlab.api | 0.00% imprecise | 9 LOC | -| models.gitlab.merge_request | 25.54% imprecise | 278 LOC | -| models.gitlab.repo | 7.84% imprecise | 51 LOC | -| models.gitlab.settings | 0.00% imprecise | 9 LOC | -| models.gitlab.utils | 41.67% imprecise | 12 LOC | -| models.groups | 16.21% imprecise | 327 LOC | -| models.reviews | 13.48% imprecise | 89 LOC | -| models.states | 0.00% imprecise | 15 LOC | -| models.status | 1.69% imprecise | 118 LOC | -| queuing | 46.88% imprecise | 64 LOC | -| sentry | 30.77% imprecise | 13 LOC | -| settings | 48.24% imprecise | 85 LOC | -| snapshots | 0.00% imprecise | 0 LOC | -| snapshots.snap_test_config | 4.69% imprecise | 64 LOC | -| snapshots.snap_test_pull_request | 0.69% imprecise | 875 LOC | -| storage | 36.92% imprecise | 65 LOC | -| test_billing | 38.71% imprecise | 155 LOC | -| test_commands | 62.50% imprecise | 8 LOC | -| test_config | 32.14% imprecise | 28 LOC | -| test_expression | 53.14% imprecise | 350 LOC | -| test_fingerprints | 52.17% imprecise | 23 LOC | -| test_github_context | 64.46% imprecise | 242 LOC | -| test_groups | 58.33% imprecise | 12 LOC | -| test_labels | 54.55% imprecise | 11 LOC | -| test_live | 33.33% imprecise | 18 LOC | -| test_pull_request | 31.09% imprecise | 312 LOC | -| test_review_requests | 64.43% imprecise | 253 LOC | -| test_reviews | 59.26% imprecise | 108 LOC | -| test_states | 67.86% imprecise | 84 LOC | -| test_template | 50.85% imprecise | 59 LOC | -| test_utils | 54.55% imprecise | 11 LOC | -| user_input | 0.00% imprecise | 0 LOC | -| user_input.expressions | 20.41% imprecise | 49 LOC | -| user_input.template | 9.09% imprecise | 33 LOC | -| utils | 23.68% imprecise | 38 LOC | -+-------------------------------------+-------------------+----------+ -| Total | 28.42% imprecise | 7483 LOC | -+-------------------------------------+-------------------+----------+ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..7247761 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,224 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "pathspec" +version = "0.10.3" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.0.202" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.202-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:4b79d4ba3530aee5f1de3add3096df7bd483bb3a8c8933f61fe0b16d2dbbf3d2"}, + {file = "ruff-0.0.202-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:81502b86d0608549f0cd8e5f6fb66a29f365013cdf3dfdaa6521d9307c01e614"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa5b92ab3dcf5355545adec731d64598663e5c38a94710a1a971fafdbd8ccb1"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91f38496b3194ce1335cc50f4a439e55a66fa12615d0b57c0867f9136249472c"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6003f9ecc1cc10c5cad5f71072a4d98e10af6f73de148218323429996dd7c69"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d34d9b8f495ad3f1333819a3cbfbcbf9c96c4637b132038cd0f0786bc48e354d"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52fea12733b056b74ef7d0991d1f6bdf700fa0d4c15adec91dafba14a7718a3a"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b54a64ece6e8c7113cfa26368fe55864ddf286ae18cbb02e7c5ed39e7a27c92"}, + {file = "ruff-0.0.202-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16d3708a2b758fa1b2ec2755e8850bc1e8327e1b4c15bdca7c19eb449077fdf5"}, + {file = "ruff-0.0.202-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d62322974cb8a37d5b352c0337936284002b681ab4ca164dcf97a5c5c173e0a7"}, + {file = "ruff-0.0.202-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:eeb114e0b26ad92dcd31dc62610a66a76cbf151a7822f5624eb8faf0461fae1a"}, + {file = "ruff-0.0.202-py3-none-musllinux_1_2_i686.whl", hash = "sha256:adcf5dc49eba84ae037c47809b6069d6d4606f14bab1bb64086843a9dcbf2c01"}, + {file = "ruff-0.0.202-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d7dd2d7850a88f482fd8144fdd9beeb179b2411c5203938979401a0f1ca53ea2"}, + {file = "ruff-0.0.202-py3-none-win32.whl", hash = "sha256:76f4cff5f8d39b594e9384a947a9733e847373babbe4bcaba06caa6484ade6a4"}, + {file = "ruff-0.0.202-py3-none-win_amd64.whl", hash = "sha256:68563fe0783265e694005adb81291b207fbf617635f35f34af5b481b713babfe"}, + {file = "ruff-0.0.202.tar.gz", hash = "sha256:cf9efa3a2d1a5072bae9e9c2dd453373c3169a0433809af1a0834472ea6d2fa0"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "1.26.13" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "5933bab71b4dca85670afa2c9a0ea584db96620ea4f325c3a5f46f5b81e5ed05" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..396ecd4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "commitstat" +version = "0.1.0" +description = "Store commit stats as git notes." +authors = ["Dave Gaeddert "] +license = "MIT" +readme = "README.md" + +[tool.poetry.scripts] +git-stats = "commitstat:cli.cli" + +[tool.poetry.dependencies] +python = "^3.10" +click = ">=8.0.0" +requests = "*" + +[tool.poetry.group.dev.dependencies] +black = "^22.12.0" +ruff = "^0.0.202" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..ced1b48 --- /dev/null +++ b/scripts/format @@ -0,0 +1,3 @@ +#!/bin/sh -e +black commitstat +ruff commitstat --fix-only diff --git a/scripts/install b/scripts/install index dea6cd0..5a81765 100755 --- a/scripts/install +++ b/scripts/install @@ -1,2 +1,2 @@ #!/bin/sh -ex -go mod download \ No newline at end of file +POETRY_VENV_IN_PROJECT=1 poetry install diff --git a/scripts/pre-commit b/scripts/pre-commit index 719db99..a224049 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -1,2 +1,3 @@ #!/bin/sh -e -./scripts/test +black --check commitstat +ruff commitstat diff --git a/scripts/run b/scripts/run deleted file mode 100755 index 60d2d51..0000000 --- a/scripts/run +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -e -go run ./cmd/commitstat "$@" diff --git a/scripts/test b/scripts/test deleted file mode 100755 index 9e540b9..0000000 --- a/scripts/test +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -e -go test ./... From a9893dbfaa95b6aba62028d8f79aa6053994f35c Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 17:31:16 -0600 Subject: [PATCH 02/23] Config file --- README.md | 123 +------------------------------------------ commitstat/cli.py | 114 ++++++++++++++++++++++++++++++++------- commitstat/config.py | 27 ++++++++++ commitstat/core.py | 69 ++++++++++++++++-------- poetry.lock | 52 +++++++++++++++++- pyproject.toml | 1 + 6 files changed, 220 insertions(+), 166 deletions(-) create mode 100644 commitstat/config.py diff --git a/README.md b/README.md index 23b5d20..06e4fd1 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,3 @@ # commitstat -A CI tool for reporting and comparing stats on commits and pull requests. -It can parse a stat from a file or stdin, -check whether the number has increased or decreased from the previous commit or main branch, -and then report a pass/fail status on GitHub. - -```console -$ commitstat coverage-report.txt --regex "\| Total\s*\|\s*([\d\.]+%)" --goal increase --name coverage -``` - -Can be used to check test coverage, typing coverage, file sizes, and more. -The status can be purely informative, or be set as a required status check (via GitHub branch protection) to ensure that something like test coverage doesn't get worse because of a pull request. - -This is a lightweight alternative to hosted services like [Codecov](https://about.codecov.io/) and [Coveralls](https://coveralls.io/). -All of the data that commitstat uses is stored directly in the GitHub commit status and doesn't involve any third-party services or hosting. -There aren't any visualization tools built-in but you can always store artifacts in your CI provider or look at coverage reports locally. - -![commitstat on pull request checks](https://user-images.githubusercontent.com/649496/121754532-7df7b880-cada-11eb-8b0a-7457f0bb0ed8.png) - -## Quick install - -You can install commitstat locally to test your parsing patterns, but commit statuses will only be created when run in CI. - -```console -pipx install commitstat -``` - -## Options - -### `--name` - -The name of the stat. -Will show up as `commitstat/{name}` when submitted as a GitHub commit status. -Changing the name for an existing stat will break the pass/fail comparison until the new name shows up on your main/master branch. - -### `--goal` (optional) - -Either "increase" or "decrease". This is the direction you *want* the stat to go. For example, test coverage should "increase" and if it actually decreases, then a failling status will be reported. If the stat is new or doesn't change, it is considered successful. - -If you don't specify a goal, then you'll get a successful status and the stat/change will be purely informational. - -### `--regex` (optional) - -A regular expression to parse the file/stdin for a specific value. -There should be exactly one capture group in your regular expression (using parentheses) and you can include extra characters like a percent sign "%". -The extra characters will simply be removed when comparing the values (ex. "36%" will be interpreted as "36"). -You'll receive an error if you mix units (ex. "1mb" and "1gb"). - -By default commitstat assumes the input is simply a number and a regex isn't needed (ex. `stat --format %s app.zip | commitstat -` on Ubuntu). - -## GitHub Action - -You can run commitstat right after your tests and once you have some sort of stat to parse. -Use different names to report multiple stats per commit. - -```yml -name: test - -on: [push] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - # Run your tests, generate coverage files, etc. - - - name: Install commitstat - run: curl https://raw.githubusercontent.com/dropseed/commitstat/master/install.sh | bash -s -- -b $HOME/bin - - - name: Run commitstat - run: $HOME/bin/commitstat coverage-report.txt --regex "\| Total\s*\|\s*([\d\.]+%)" --goal increase --name coverage - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -``` - -## Example uses - -### mypy - -Parse the [mypy](https://mypy.readthedocs.io/en/stable/command_line.html#report-generation) imprecision text report (lower number is better). - -```console -$ mypy src --ignore-missing-imports --no-incremental --txt-report ./.reports/mypy -$ commitstat .reports/mypy/index.txt --regex "\| Total\s*\|\s*([\d\.]+%)" --goal decrease --name mypy -``` - -### pytest-cov - -Parse the [pytest-cov](https://github.com/pytest-dev/pytest-cov) default HTML report for the total coverage percentage. - -```console -$ pytest --cov=src --cov-report=html:.reports/src/pytest src -$ commitstat .reports/pytest/index.html --regex "(\d+%)<\/span>" --goal increase --name pytest -``` - -### Go test coverage - -Parse total test coverage from the [built-in go coverage tool](https://blog.golang.org/cover). - -```console -$ go test ./... -coverprofile=coverage.out -$ go tool cover -func coverage.out | commitstat - --regex "total:\s+\(statements\)\s+([\d\.]+%)" --goal increase --name coverage -``` - -### File sizes - -Report the size of a file (in bytes) using `stat` on Ubuntu (note that the options for stat can differ depending on your OS). - -```console -$ stat --format %s app.zip | commitstat - -``` - -## How it works - -Stats are stored directly in GitHub through the commit status API. -The stat is always the first number in the description which makes it easy to parse. - - - -![commitstat git](https://user-images.githubusercontent.com/649496/121432738-a9df3680-c940-11eb-9a4f-a5be6e3fb05b.png) +A lightweight tool to store metrics and stats directly in your git repo as [git notes](https://git-scm.com/docs/git-notes). diff --git a/commitstat/cli.py b/commitstat/cli.py index 85e4fc4..d31ed6c 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -1,6 +1,7 @@ import subprocess import click from .core import Stats +from .config import Config @click.group() @@ -9,12 +10,37 @@ def cli(): @cli.command() -@click.option("--key", "-k", required=True) -@click.option("--value", "-v", required=True) # value as arg so it's pipeable? +@click.option("--key", "-k", "keys", default=[], multiple=True) @click.option("--commitish", default="HEAD") @click.option("--quiet", "-q", is_flag=True) -def save(key, value, commitish, quiet): - Stats().save(key=key, value=value, commitish=commitish, quiet=quiet) +def save(keys, commitish, quiet): + """Save a stat for a commit""" + config = Config.load_yaml() + if not keys: + keys = config.stats_keys() + + stats = Stats() + + for key in keys: + try: + command = config.command_for_stat(key) + except KeyError: + click.secho(f"Unknown stat key: {key}", fg="red") + exit(1) + + click.echo(f"Generating value for {key}: ", nl=False) + value = subprocess.check_output(command, shell=True).decode("utf-8").strip() + if value: + click.secho(str(value), fg="green") + stats.save(key=key, value=value, commitish=commitish) + else: + click.secho(f"Skipping empty value for {key}", fg="yellow") + + if not quiet: + click.secho(f"\nStats for {commitish}:", bold=True) + stats.show(commitish) + + # TODO if CI and stat config has a "goal", then create github status? @cli.command( @@ -23,14 +49,15 @@ def save(key, value, commitish, quiet): ) ) @click.option("--key", "-k", "keys", default=[], multiple=True) -@click.option("--default-value", "-d", default="0") @click.option("--summarize", "-s", is_flag=True) @click.option("--values-only", is_flag=True) @click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) -def log(keys, default_value, values_only, summarize, git_log_args): +def log(keys, values_only, summarize, git_log_args): + """Log stats for commits matching git log args""" + config = Config.load_yaml() Stats().log( keys=keys, - default_value=default_value, + config=config, values_only=values_only, summarize=summarize, git_log_args=list(git_log_args), @@ -40,6 +67,7 @@ def log(keys, default_value, values_only, summarize, git_log_args): @cli.command() @click.argument("commitish", default="HEAD") def show(commitish): + """Show stats for a commit""" try: Stats().show(commitish) except subprocess.CalledProcessError as e: @@ -48,11 +76,17 @@ def show(commitish): @cli.command() def push(): - Stats().push() + """Push stats to remote""" + try: + Stats().push() + except subprocess.CalledProcessError as e: + click.secho("\nHave you created any stats yet?", fg="yellow") + exit(e.returncode) @cli.command() def fetch(): + """Fetch stats from remote""" Stats().fetch() @@ -61,10 +95,10 @@ def fetch(): ignore_unknown_options=True, ) ) -@click.option("--key", "-k", required=True) -@click.option("--command", "-c", required=True) +@click.option("--key", "-k", "keys", default=[], multiple=True) @click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) -def regen(key, command, git_log_args): +def regen(keys, git_log_args): + """Regenerate stats for all commits matching git log args""" current_branch = ( subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) .decode("utf-8") @@ -82,24 +116,37 @@ def regen(key, command, git_log_args): commits = output.splitlines() + config = Config.load_yaml() + if not keys: + keys = config.stats_keys() + if not click.prompt( - f'Regenerate "{key}" stat for {len(commits)} commits?', default=True + f"Regenerate {list(keys)} stats for {len(commits)} commits?", default=True ): exit(1) try: - with click.progressbar(commits, show_pos=True, show_eta=True) as items: + with click.progressbar( + commits, show_pos=True, show_eta=True, label="Commits" + ) as items: for commit in items: + # TODO --missing-only option to only fill in + # blanks (skip checkout etc. if nothing needed) + subprocess.check_call( ["git", "checkout", commit], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) - value = ( - subprocess.check_output(command, shell=True).decode("utf-8").strip() - ) - Stats().save(key=key, value=value, commitish=commit, quiet=True) + for key in keys: + command = config.command_for_stat(key) + value = ( + subprocess.check_output(command, shell=True) + .decode("utf-8") + .strip() + ) + Stats().save(key=key, value=value, commitish=commit) finally: # Make sure we reset to the ref where we started subprocess.check_call( @@ -110,10 +157,37 @@ def regen(key, command, git_log_args): @cli.command() -@click.option("--key", "-k", default="") +@click.option("--key", "-k", "keys", default=[], multiple=True) @click.option("--commitish", default="HEAD") -def delete(key, commitish): - Stats().delete(key, commitish) +def delete(keys, commitish): + """Delete a single stat""" + config = Config.load_yaml() + if not keys: + keys = config.stats_keys() + + stats = Stats() + for key in keys: + stats.delete(key, commitish) + + try: + stats.show(commitish) + except subprocess.CalledProcessError: + # Stats probably all deleted + pass + + +@cli.command() +@click.option("--remote", is_flag=True) +def clear(remote): + """Delete all existing stats""" + click.confirm("Are you sure you want to clear all existing stats?", abort=True) + + Stats().clear(remote=remote) + + if remote: + click.secho("Remote stats cleared", fg="green") + else: + click.secho("Local stats cleared", fg="green") if __name__ == "__main__": diff --git a/commitstat/config.py b/commitstat/config.py new file mode 100644 index 0000000..409f24c --- /dev/null +++ b/commitstat/config.py @@ -0,0 +1,27 @@ +import yaml + + +DEFAULT_FILENAME = "commitstat.yml" + + +class Config: + def __init__(self, *, stats): + self.stats = stats + + @classmethod + def load_dict(cls, data): + return cls(stats=data["stats"]) + + @classmethod + def load_yaml(cls, path=DEFAULT_FILENAME): + with open(path) as f: + return cls.load_dict(yaml.safe_load(f)) + + def stats_keys(self): + return self.stats.keys() + + def command_for_stat(self, key): + return self.stats[key]["run"] + + def default_for_stat(self, key): + return self.stats[key].get("default", 0) diff --git a/commitstat/core.py b/commitstat/core.py index 50e6d98..c735615 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -4,10 +4,12 @@ class Stats: def __init__(self): - self.stats_ref = "stats" - self.stats_ref_path = "refs/notes/stats" + self.stats_ref = "commitstats" + self.stats_ref_path = "refs/notes/commitstats" - def save(self, *, key, value, commitish="HEAD", quiet=False): + def save(self, *, key, value, commitish="HEAD"): + # TODO use the new CommitStats classes for this? + # would be nice to keep existing order for stats though in message stat_line = f"{key}: {value}" if existing_stats := self.get(commitish): @@ -47,9 +49,6 @@ def save(self, *, key, value, commitish="HEAD", quiet=False): stderr=subprocess.DEVNULL, ) - if not quiet: - self.show(commitish) - def delete(self, key, commitish="HEAD"): if existing_stats := self.get(commitish): edited_stat_lines = [] @@ -58,23 +57,45 @@ def delete(self, key, commitish="HEAD"): if not line.startswith(f"{key}:"): edited_stat_lines.append(line) - message = "\n".join(edited_stat_lines) + if edited_stat_lines: + message = "\n".join(edited_stat_lines) + + subprocess.check_call( + [ + "git", + "notes", + "--ref", + self.stats_ref, + "add", + "--force", + "--message", + message, + commitish, + ], + # Don't show the "Overwriting existing notes for commit" + stderr=subprocess.DEVNULL, + ) + else: + subprocess.check_call( + [ + "git", + "notes", + "--ref", + self.stats_ref, + "remove", + commitish, + ], + # Don't show the "Overwriting existing notes for commit" + stderr=subprocess.DEVNULL, + ) + def clear(self, remote): + if remote: subprocess.check_call( - [ - "git", - "notes", - "--ref", - self.stats_ref, - "add", - "--force", - "--message", - message, - commitish, - ], - # Don't show the "Overwriting existing notes for commit" - stderr=subprocess.DEVNULL, + ["git", "push", "--delete", "origin", self.stats_ref_path] ) + else: + subprocess.check_call(["git", "update-ref", "-d", self.stats_ref_path]) def get(self, commitish="HEAD"): try: @@ -90,7 +111,7 @@ def show(self, commitish="HEAD"): ["git", "notes", "--ref", self.stats_ref, "show", commitish] ) - def log(self, *, keys, default_value, values_only, summarize, git_log_args=[]): + def log(self, *, keys, config, values_only, summarize, git_log_args=[]): stats = CommitStats() output = subprocess.check_output( @@ -121,9 +142,11 @@ def log(self, *, keys, default_value, values_only, summarize, git_log_args=[]): key, value = line.split(":", 1) if key in keys or not keys: - # TODO default value may have to be per-key... config file? stats.add( - commit=commit, key=key, value=value, default_value=default_value + commit=commit, + key=key, + value=value, + default_value=config.default_for_stat(key), ) stats.print(values_only=values_only) diff --git a/poetry.lock b/poetry.lock index 7247761..5066d5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -141,6 +141,56 @@ files = [ docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + [[package]] name = "requests" version = "2.28.1" @@ -221,4 +271,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5933bab71b4dca85670afa2c9a0ea584db96620ea4f325c3a5f46f5b81e5ed05" +content-hash = "b633c6964a7e472a3d990eaaae513604b43f28e1ec2af4b2b6b877c01743043a" diff --git a/pyproject.toml b/pyproject.toml index 396ecd4..ece6769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ git-stats = "commitstat:cli.cli" python = "^3.10" click = ">=8.0.0" requests = "*" +pyyaml = "^6.0" [tool.poetry.group.dev.dependencies] black = "^22.12.0" From cbf8ebd08ad30156f160b71e23f2538092aa6c30 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:03:56 -0600 Subject: [PATCH 03/23] Add CI command, detect no change, regen stash --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ commitstat/cli.py | 32 ++++++++++++++++++++++++++++++-- commitstat/core.py | 2 ++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 06e4fd1..d4fd9e2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,48 @@ # commitstat A lightweight tool to store metrics and stats directly in your git repo as [git notes](https://git-scm.com/docs/git-notes). + +The easiest way to install this for yourself or in CI is with [pipx](https://pypa.github.io/pipx/): + +```console +pipx install git+https://github.com/dropseed/commitstat@git-stats +``` + +The tool is then available as: + +```console +$ git stats +Usage: git-stats [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + clear Delete all existing stats + delete Delete a single stat + fetch Fetch stats from remote + log Log stats for commits matching git log args + push Push stats to remote + regen Regenerate stats for all commits matching git log args + save Save a stat for a commit + show Show stats for a commit +``` + +Stats are generated by running shell commands. +You can save the names of your stats and their commands in `commitstat.yml`: + +```yaml +# commitstat.yml +stats: + todos: + run: | + grep "TODO" -r app -c | awk -F: '{sum+=$NF} END {print sum}' + num-python-deps: + run: | + grep "\[\[package\]\]" poetry.lock -c + coverage: + default: 0% + run: | + poetry run coverage -m pytest > /dev/null + poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' +``` diff --git a/commitstat/cli.py b/commitstat/cli.py index d31ed6c..44b59ee 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -96,9 +96,17 @@ def fetch(): ) ) @click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--stash", + is_flag=True, + help="Stash your local changes after the config is loaded", +) @click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) -def regen(keys, git_log_args): - """Regenerate stats for all commits matching git log args""" +def regen(keys, stash, git_log_args): + """ + Regenerate stats for all commits matching git log args + (using the config as it existed pre-stash) + """ current_branch = ( subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) .decode("utf-8") @@ -125,6 +133,9 @@ def regen(keys, git_log_args): ): exit(1) + if stash: + subprocess.check_call(["git", "stash"], stdout=subprocess.DEVNULL) + try: with click.progressbar( commits, show_pos=True, show_eta=True, label="Commits" @@ -154,6 +165,8 @@ def regen(keys, git_log_args): stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) + if stash: + subprocess.check_call(["git", "stash", "pop"], stdout=subprocess.DEVNULL) @cli.command() @@ -190,5 +203,20 @@ def clear(remote): click.secho("Local stats cleared", fg="green") +@cli.command() +@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.pass_context +def ci(ctx, keys): + """All-in-one fetch, save, push""" + click.secho("Fetching stats from remote...", fg="cyan") + ctx.invoke(fetch) + + click.secho("\nSaving stats...", fg="cyan") + ctx.invoke(save, keys=keys) + + click.secho("\nPushing stats to remote...", fg="cyan") + ctx.invoke(push) + + if __name__ == "__main__": cli() diff --git a/commitstat/core.py b/commitstat/core.py index c735615..ec22891 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -30,6 +30,8 @@ def save(self, *, key, value, commitish="HEAD"): edited_stat_lines.append(stat_line) message = "\n".join(edited_stat_lines) + if message.strip() == existing_stats.strip(): + return else: message = stat_line From b176e8187852f5b11dcc20a7fd3cce79d1890453 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:08:44 -0600 Subject: [PATCH 04/23] Delete notes then add instead of force --- README.md | 6 ++++++ commitstat/core.py | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d4fd9e2..cc7e69e 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,9 @@ stats: poetry run coverage -m pytest > /dev/null poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' ``` + +In CI: + +``` +pipx run --spec git+https://github.com/dropseed/commitstat@git-stats git-stats ci +``` diff --git a/commitstat/core.py b/commitstat/core.py index ec22891..f52ff55 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -32,6 +32,17 @@ def save(self, *, key, value, commitish="HEAD"): message = "\n".join(edited_stat_lines) if message.strip() == existing_stats.strip(): return + + subprocess.check_call( + [ + "git", + "notes", + "--ref", + self.stats_ref, + "remove", + commitish, + ], + ) else: message = stat_line @@ -42,13 +53,10 @@ def save(self, *, key, value, commitish="HEAD"): "--ref", self.stats_ref, "add", - "--force", "--message", message, commitish, ], - # Don't show the "Overwriting existing notes for commit" - stderr=subprocess.DEVNULL, ) def delete(self, key, commitish="HEAD"): @@ -87,8 +95,6 @@ def delete(self, key, commitish="HEAD"): "remove", commitish, ], - # Don't show the "Overwriting existing notes for commit" - stderr=subprocess.DEVNULL, ) def clear(self, remote): From 460a61fee9a7f76e34854c9742aaf3395861ccf2 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:12:59 -0600 Subject: [PATCH 05/23] Set git user.name and user.email in ci --- commitstat/cli.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 44b59ee..377a34d 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -205,9 +205,19 @@ def clear(remote): @cli.command() @click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option("--git-author", default="github-actions") +@click.option("--git-email", default="github-actions@github.com") @click.pass_context -def ci(ctx, keys): +def ci(ctx, keys, git_author, git_email): """All-in-one fetch, save, push""" + click.secho("Setting git user.name and user.email...", fg="cyan") + subprocess.check_call( + ["git", "config", "user.name", git_author], + ) + subprocess.check_call( + ["git", "config", "user.email", git_email], + ) + click.secho("Fetching stats from remote...", fg="cyan") ctx.invoke(fetch) From 2111109c108fa0adefcaee2b4175867671f5567f Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:13:56 -0600 Subject: [PATCH 06/23] Change --git-author to --git-name --- commitstat/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 377a34d..02dc3bc 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -205,14 +205,14 @@ def clear(remote): @cli.command() @click.option("--key", "-k", "keys", default=[], multiple=True) -@click.option("--git-author", default="github-actions") +@click.option("--git-name", default="github-actions") @click.option("--git-email", default="github-actions@github.com") @click.pass_context -def ci(ctx, keys, git_author, git_email): +def ci(ctx, keys, git_name, git_email): """All-in-one fetch, save, push""" click.secho("Setting git user.name and user.email...", fg="cyan") subprocess.check_call( - ["git", "config", "user.name", git_author], + ["git", "config", "user.name", git_name], ) subprocess.check_call( ["git", "config", "user.email", git_email], From 086c4710551d92d0ee9641b9443ac5fa25cde3e1 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:17:24 -0600 Subject: [PATCH 07/23] Update fetch command and hide delete output in save --- commitstat/cli.py | 1 + commitstat/core.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 02dc3bc..96751f5 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -210,6 +210,7 @@ def clear(remote): @click.pass_context def ci(ctx, keys, git_name, git_email): """All-in-one fetch, save, push""" + # TODO pass these to save instead of setting config? click.secho("Setting git user.name and user.email...", fg="cyan") subprocess.check_call( ["git", "config", "user.name", git_name], diff --git a/commitstat/core.py b/commitstat/core.py index f52ff55..eb96a48 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -42,6 +42,7 @@ def save(self, *, key, value, commitish="HEAD"): "remove", commitish, ], + stdout=subprocess.DEVNULL, ) else: message = stat_line @@ -163,4 +164,6 @@ def push(self): subprocess.check_call(["git", "push", "origin", self.stats_ref_path]) def fetch(self): - subprocess.check_call(["git", "fetch", "origin", self.stats_ref_path]) + subprocess.check_call( + ["git", "fetch", "origin", f"{self.stats_ref_path}:{self.stats_ref_path}"] + ) From 9eb59e1b74ea38dbec4c8bef053790059dd93dbb Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:27:32 -0600 Subject: [PATCH 08/23] More README --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc7e69e..ee9dfa8 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,49 @@ stats: poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' ``` -In CI: +To generate stats for a commit, run: + +```console +git stats save +``` + +This saves the stats locally, which you could push with `git stats push`. +Typically though you'll run this in CI. + +So you can delete your local stats with `git stats delete`, +and then in GitHub Actions, for example, you can do: + +```yaml +name: test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + # ... + - run: pipx run --spec git+https://github.com/dropseed/commitstat@git-stats git-stats ci ``` -pipx run --spec git+https://github.com/dropseed/commitstat@git-stats git-stats ci + +## Retroactive stats + +You're probably introducing this into an existing project, +and will want to generate some stats for existing commits. + +To do that, use: + +```console +git stats regen +``` + +You can specify a key(s) with `git stats regen --key todos`, +and any additional options will be used to select the commits to regenerate stats for (via `git log`). +So, you can regenerate stats for the latest 50 commits with: + +```console +git stats regen -n 50 ``` From 0bbfce9ae901768ee4f67cd1553152b48d64a0b5 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:33:16 -0600 Subject: [PATCH 09/23] Update CI example --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ee9dfa8..d6d8111 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,10 @@ stats: coverage: default: 0% run: | - poetry run coverage -m pytest > /dev/null + if [ "$CI" != "true" ]; then + # Don't need to run the test itself in CI (already ran) + poetry run coverage -m pytest > /dev/null + fi poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' ``` From 204e722699380e7694f5d262fc24c9783a8de362 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:35:40 -0600 Subject: [PATCH 10/23] Spark example --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index d6d8111..4adda63 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,13 @@ So, you can regenerate stats for the latest 50 commits with: ```console git stats regen -n 50 ``` + +## Sparklines + +Some reporting/charting tools will likely be built-in (and integrated w/ GitHub Actions), +but for now you can pipe output to something like [spark](https://github.com/holman/spark): + +```console +$ git-stats log --key todos --values-only -n 50 --reverse | spark +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇▇▇███████████████████████████████ +``` From 43083c20f8a9aa7a6cb228f9e2fe2ae38ebe8b13 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:38:05 -0600 Subject: [PATCH 11/23] $ console syntax? --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4adda63..0aaa13a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A lightweight tool to store metrics and stats directly in your git repo as [git The easiest way to install this for yourself or in CI is with [pipx](https://pypa.github.io/pipx/): ```console -pipx install git+https://github.com/dropseed/commitstat@git-stats +$ pipx install git+https://github.com/dropseed/commitstat@git-stats ``` The tool is then available as: @@ -53,7 +53,7 @@ stats: To generate stats for a commit, run: ```console -git stats save +$ git stats save ``` This saves the stats locally, which you could push with `git stats push`. @@ -86,7 +86,7 @@ and will want to generate some stats for existing commits. To do that, use: ```console -git stats regen +$ git stats regen ``` You can specify a key(s) with `git stats regen --key todos`, @@ -94,7 +94,7 @@ and any additional options will be used to select the commits to regenerate stat So, you can regenerate stats for the latest 50 commits with: ```console -git stats regen -n 50 +$ git stats regen -n 50 ``` ## Sparklines From 80ad8de52bb5c8590c42a2b0e4f40f03a51709f0 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:44:46 -0600 Subject: [PATCH 12/23] Add test command --- README.md | 18 +++++++++--------- commitstat/cli.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0aaa13a..0d60adb 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,16 @@ Options: --help Show this message and exit. Commands: + ci All-in-one fetch, save, push clear Delete all existing stats delete Delete a single stat fetch Fetch stats from remote log Log stats for commits matching git log args push Push stats to remote - regen Regenerate stats for all commits matching git log args + regen Regenerate stats for all commits matching git log args (using... save Save a stat for a commit show Show stats for a commit + test Output stats but don't save them ``` Stats are generated by running shell commands. @@ -50,17 +52,14 @@ stats: poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' ``` -To generate stats for a commit, run: +To test your stats on your working directory, run: ```console -$ git stats save +$ git stats test ``` -This saves the stats locally, which you could push with `git stats push`. -Typically though you'll run this in CI. - -So you can delete your local stats with `git stats delete`, -and then in GitHub Actions, for example, you can do: +You can `save` and `push` stats locally, +but typically you'll have this running in CI automatically: ```yaml name: test @@ -103,6 +102,7 @@ Some reporting/charting tools will likely be built-in (and integrated w/ GitHub but for now you can pipe output to something like [spark](https://github.com/holman/spark): ```console -$ git-stats log --key todos --values-only -n 50 --reverse | spark +$ git stats fetch +$ git stats log --key todos --values-only -n 50 --reverse | spark ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇▇▇███████████████████████████████ ``` diff --git a/commitstat/cli.py b/commitstat/cli.py index 96751f5..36a21c5 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -9,6 +9,29 @@ def cli(): pass +@cli.command() +@click.option("--key", "-k", "keys", default=[], multiple=True) +def test(keys): + """Output stats but don't save them""" + config = Config.load_yaml() + if not keys: + keys = config.stats_keys() + + for key in keys: + try: + command = config.command_for_stat(key) + except KeyError: + click.secho(f"Unknown stat key: {key}", fg="red") + exit(1) + + click.echo(f"Generating value for {key}: ", nl=False) + value = subprocess.check_output(command, shell=True).decode("utf-8").strip() + if value: + click.secho(str(value), fg="green") + else: + click.secho(f"Skipping empty value for {key}", fg="yellow") + + @cli.command() @click.option("--key", "-k", "keys", default=[], multiple=True) @click.option("--commitish", default="HEAD") From de9b5e3599a00c80675cc3e9248df71c55f61935 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 18:50:25 -0600 Subject: [PATCH 13/23] Simpler example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d60adb..7dacc70 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ stats: # Don't need to run the test itself in CI (already ran) poetry run coverage -m pytest > /dev/null fi - poetry run coverage report --data-file .forge/.coverage | tail -n 1 | awk '{print $4}' + poetry run coverage report | tail -n 1 | awk '{print $4}' ``` To test your stats on your working directory, run: From 3f4c318aa32e4c61f73753cf42d8e09bae62224b Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 22:10:28 -0600 Subject: [PATCH 14/23] Add sparklines, min/max/avg, stat % type --- README.md | 6 ++-- commitstat/cli.py | 15 ++++++--- commitstat/config.py | 3 ++ commitstat/core.py | 39 +++++++++++++++++----- commitstat/stats.py | 77 +++++++++++++++++++++++++++++++++++--------- poetry.lock | 27 +++++++++++++++- pyproject.toml | 1 + 7 files changed, 134 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7dacc70..245eeac 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,9 @@ $ git stats regen -n 50 ## Sparklines -Some reporting/charting tools will likely be built-in (and integrated w/ GitHub Actions), -but for now you can pipe output to something like [spark](https://github.com/holman/spark): - ```console $ git stats fetch -$ git stats log --key todos --values-only -n 50 --reverse | spark +$ git stats log --key todos --values-only -n 50 --reverse --format sparklines +todos (min 0, max 43, avg 25.4) ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇▇▇███████████████████████████████ ``` diff --git a/commitstat/cli.py b/commitstat/cli.py index 36a21c5..654687f 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -72,17 +72,21 @@ def save(keys, commitish, quiet): ) ) @click.option("--key", "-k", "keys", default=[], multiple=True) -@click.option("--summarize", "-s", is_flag=True) @click.option("--values-only", is_flag=True) +@click.option( + "--format", "fmt", default="tsv", type=click.Choice(["tsv", "csv", "sparklines"]) +) @click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) -def log(keys, values_only, summarize, git_log_args): +def log(keys, values_only, fmt, git_log_args): """Log stats for commits matching git log args""" config = Config.load_yaml() + if not keys: + keys = config.stats_keys() Stats().log( keys=keys, config=config, values_only=values_only, - summarize=summarize, + fmt=fmt, git_log_args=list(git_log_args), ) @@ -108,9 +112,10 @@ def push(): @cli.command() -def fetch(): +@click.option("--force", "-f", is_flag=True) +def fetch(force): """Fetch stats from remote""" - Stats().fetch() + Stats().fetch(force=force) @cli.command( diff --git a/commitstat/config.py b/commitstat/config.py index 409f24c..4238cfa 100644 --- a/commitstat/config.py +++ b/commitstat/config.py @@ -23,5 +23,8 @@ def stats_keys(self): def command_for_stat(self, key): return self.stats[key]["run"] + def type_for_stat(self, key): + return self.stats[key].get("type", "number") + def default_for_stat(self, key): return self.stats[key].get("default", 0) diff --git a/commitstat/core.py b/commitstat/core.py index eb96a48..4c63b19 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -120,7 +120,7 @@ def show(self, commitish="HEAD"): ["git", "notes", "--ref", self.stats_ref, "show", commitish] ) - def log(self, *, keys, config, values_only, summarize, git_log_args=[]): + def log(self, *, keys, config, values_only, fmt, git_log_args=[]): stats = CommitStats() output = subprocess.check_output( @@ -134,6 +134,7 @@ def log(self, *, keys, config, values_only, summarize, git_log_args=[]): ).decode("utf-8") commit = "" + keys_in_commit = set() for line in output.splitlines(): if commit is None: @@ -142,7 +143,17 @@ def log(self, *, keys, config, values_only, summarize, git_log_args=[]): continue if line == "COMMIT": + remaining_keys = set(keys) - keys_in_commit + for key in remaining_keys: + # Fill empty values + stats.add( + commit=commit, + key=key, + value=config.default_for_stat(key), + type=config.type_for_stat(key), + ) commit = None + keys_in_commit = set() continue if not line.strip(): @@ -150,20 +161,32 @@ def log(self, *, keys, config, values_only, summarize, git_log_args=[]): key, value = line.split(":", 1) - if key in keys or not keys: + if key in keys: + keys_in_commit.add(key) stats.add( commit=commit, key=key, value=value, - default_value=config.default_for_stat(key), + type=config.type_for_stat(key), ) - stats.print(values_only=values_only) + if fmt == "tsv": + stats.print(values_only=values_only, sep="\t") + elif fmt == "csv": + stats.print(values_only=values_only, sep=",") + elif fmt == "sparklines": + stats.sparklines() def push(self): subprocess.check_call(["git", "push", "origin", self.stats_ref_path]) - def fetch(self): - subprocess.check_call( - ["git", "fetch", "origin", f"{self.stats_ref_path}:{self.stats_ref_path}"] - ) + def fetch(self, force=False): + args = [ + "git", + "fetch", + "origin", + f"{self.stats_ref_path}:{self.stats_ref_path}", + ] + if force: + args.append("--force") + subprocess.check_call(args) diff --git a/commitstat/stats.py b/commitstat/stats.py index 5f39512..7e9db32 100644 --- a/commitstat/stats.py +++ b/commitstat/stats.py @@ -1,3 +1,9 @@ +import click +from sparklines import sparklines + +from collections import OrderedDict + + class CommitStats: def __init__(self): self.stats = {} # Keep a reference to stats by key @@ -6,39 +12,78 @@ def __init__(self): def append_commit(self, commit): self.commits.append(commit) - def add(self, *, commit, key, value, default_value): - self.stats.setdefault(key, CommitStat(default_value=default_value)).add( - commit, value - ) + def add(self, *, commit, key, value, type): + self.stats.setdefault(key, CommitStat(type=type)).add(commit, value) - def print(self, values_only=False, delimiter="\t"): + def print(self, values_only=False, sep="\t"): if not values_only: - print("commit", end=delimiter) + print("commit", end=sep) for key in self.stats.keys(): - print(key, end=delimiter) + print(key, end=sep) print() for commit in self.commits: if not values_only: - print(commit, end=delimiter) + print(commit, end=sep) for stat in self.stats.values(): - print(stat.get(commit), end=delimiter) + print(stat.get(commit), end=sep) + print() + + def sparklines(self): + for key, stat in self.stats.items(): + click.secho( + f"{click.style(key, bold=True)} " + + f"(min {stat.min()}, max {stat.max()}, avg {stat.avg()})" + ) + for s in sparklines(stat.commit_values.values()): + print(s) print() class CommitStat: """A single stat for multiple commits, stored by commit sha""" - def __init__(self, default_value=""): + def __init__(self, type="number"): # self.key = key # doesn't need to know this? - self.commit_values = {} - self.default_value = default_value + self.commit_values = OrderedDict() + self.type = type + + def parse_value(self, value): + if isinstance(value, str): + # Strip whitespace + value = value.strip() + + if self.type == "%": + value = value.rstrip("%") + + if "." in value: + value = float(value) + else: + value = int(value) + + return value def add(self, commit, value): - value = value.strip() # Strip whitespace - # TODO parse units? - self.commit_values[commit] = value + self.commit_values[commit] = self.parse_value(value) def get(self, commit): - return self.commit_values.get(commit, self.default_value) + return self.commit_values[commit] + + def min(self): + v = min(self.commit_values.values()) + return self.format_value(v) + + def max(self): + v = max(self.commit_values.values()) + return self.format_value(v) + + def avg(self): + avg = sum(self.commit_values.values()) / len(self.commit_values.values()) + avg = round(avg, 2) + return self.format_value(avg) + + def format_value(self, value): + if self.type == "%": + return f"{value}%" + return value diff --git a/poetry.lock b/poetry.lock index 5066d5e..502fc32 100644 --- a/poetry.lock +++ b/poetry.lock @@ -89,6 +89,17 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] + [[package]] name = "idna" version = "3.4" @@ -239,6 +250,20 @@ files = [ {file = "ruff-0.0.202.tar.gz", hash = "sha256:cf9efa3a2d1a5072bae9e9c2dd453373c3169a0433809af1a0834472ea6d2fa0"}, ] +[[package]] +name = "sparklines" +version = "0.4.2" +description = "Generate sparklines for numbers using Unicode characters only." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "sparklines-0.4.2.tar.gz", hash = "sha256:7c33fd6dc8b277b1fd729b31428df58a785c68be58202f439128e815f1dcae88"}, +] + +[package.dependencies] +future = "*" + [[package]] name = "tomli" version = "2.0.1" @@ -271,4 +296,4 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b633c6964a7e472a3d990eaaae513604b43f28e1ec2af4b2b6b877c01743043a" +content-hash = "ff341deea927df0e43e2ea4574a3c2a7098c94ff368e78cfa55a94ba8154359c" diff --git a/pyproject.toml b/pyproject.toml index ece6769..d88e9b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ python = "^3.10" click = ">=8.0.0" requests = "*" pyyaml = "^6.0" +sparklines = "^0.4.2" [tool.poetry.group.dev.dependencies] black = "^22.12.0" From 82772ae03d68c246421296b8bb1a3454697bef50 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 22:32:39 -0600 Subject: [PATCH 15/23] Catch failed checkout, fix empty 0 in first log --- commitstat/cli.py | 16 +++++++++++----- commitstat/core.py | 23 +++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 654687f..077b9fa 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -172,11 +172,17 @@ def regen(keys, stash, git_log_args): # TODO --missing-only option to only fill in # blanks (skip checkout etc. if nothing needed) - subprocess.check_call( - ["git", "checkout", commit], - stderr=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - ) + try: + subprocess.check_call( + ["git", "checkout", commit], + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + ) + except subprocess.CalledProcessError: + raise Exception( + f"Failed to checkout {commit}... " + + "if you have edited your config, try --stash." + ) for key in keys: command = config.command_for_stat(key) diff --git a/commitstat/core.py b/commitstat/core.py index 4c63b19..b453fe5 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -43,6 +43,7 @@ def save(self, *, key, value, commitish="HEAD"): commitish, ], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) else: message = stat_line @@ -133,7 +134,7 @@ def log(self, *, keys, config, values_only, fmt, git_log_args=[]): ] ).decode("utf-8") - commit = "" + commit = False keys_in_commit = set() for line in output.splitlines(): @@ -143,15 +144,17 @@ def log(self, *, keys, config, values_only, fmt, git_log_args=[]): continue if line == "COMMIT": - remaining_keys = set(keys) - keys_in_commit - for key in remaining_keys: - # Fill empty values - stats.add( - commit=commit, - key=key, - value=config.default_for_stat(key), - type=config.type_for_stat(key), - ) + if commit is not False: + # Fill empty values, but not in very first line + remaining_keys = set(keys) - keys_in_commit + for key in remaining_keys: + # Fill empty values + stats.add( + commit=commit, + key=key, + value=config.default_for_stat(key), + type=config.type_for_stat(key), + ) commit = None keys_in_commit = set() continue From cdbfd07110f7b737722a32c4a18831a8e669927c Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 22:51:24 -0600 Subject: [PATCH 16/23] Fix missing stat fill logic --- commitstat/core.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/commitstat/core.py b/commitstat/core.py index b453fe5..d07d35b 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -135,28 +135,23 @@ def log(self, *, keys, config, values_only, fmt, git_log_args=[]): ).decode("utf-8") commit = False - keys_in_commit = set() for line in output.splitlines(): if commit is None: commit = line stats.append_commit(commit) + # Fill empty values first for all the keys we expect + for key in keys: + stats.add( + commit=commit, + key=key, + value=config.default_for_stat(key), + type=config.type_for_stat(key), + ) continue if line == "COMMIT": - if commit is not False: - # Fill empty values, but not in very first line - remaining_keys = set(keys) - keys_in_commit - for key in remaining_keys: - # Fill empty values - stats.add( - commit=commit, - key=key, - value=config.default_for_stat(key), - type=config.type_for_stat(key), - ) commit = None - keys_in_commit = set() continue if not line.strip(): @@ -165,7 +160,6 @@ def log(self, *, keys, config, values_only, fmt, git_log_args=[]): key, value = line.split(":", 1) if key in keys: - keys_in_commit.add(key) stats.add( commit=commit, key=key, From 3aabc58c040c82f6fe7872e69d2d422080797051 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Fri, 30 Dec 2022 22:57:18 -0600 Subject: [PATCH 17/23] Remove extra options from save --- commitstat/cli.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 077b9fa..04aa14b 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -34,10 +34,10 @@ def test(keys): @cli.command() @click.option("--key", "-k", "keys", default=[], multiple=True) -@click.option("--commitish", default="HEAD") -@click.option("--quiet", "-q", is_flag=True) -def save(keys, commitish, quiet): - """Save a stat for a commit""" +def save(keys): + """Save stat for the current commit""" + commitish = "HEAD" + config = Config.load_yaml() if not keys: keys = config.stats_keys() @@ -59,9 +59,8 @@ def save(keys, commitish, quiet): else: click.secho(f"Skipping empty value for {key}", fg="yellow") - if not quiet: - click.secho(f"\nStats for {commitish}:", bold=True) - stats.show(commitish) + click.secho(f"\nStats for {commitish}:", bold=True) + stats.show(commitish) # TODO if CI and stat config has a "goal", then create github status? From 8e01145f25a5dba30b182234f3cb804a0dc25fc6 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Wed, 11 Jan 2023 23:02:49 -0600 Subject: [PATCH 18/23] Add --missing-only to regen, regen missing for latest 10 commits by default in CI --- commitstat/cli.py | 107 ++++++++++++++++++++++++++++++++++---------- commitstat/core.py | 37 ++++++++++----- commitstat/stats.py | 26 +++++++++-- 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 04aa14b..70f9e54 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -10,7 +10,14 @@ def cli(): @cli.command() -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) def test(keys): """Output stats but don't save them""" config = Config.load_yaml() @@ -33,7 +40,14 @@ def test(keys): @cli.command() -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) def save(keys): """Save stat for the current commit""" commitish = "HEAD" @@ -70,7 +84,14 @@ def save(keys): ignore_unknown_options=True, ) ) -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) @click.option("--values-only", is_flag=True) @click.option( "--format", "fmt", default="tsv", type=click.Choice(["tsv", "csv", "sparklines"]) @@ -122,14 +143,22 @@ def fetch(force): ignore_unknown_options=True, ) ) -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) @click.option( "--stash", is_flag=True, help="Stash your local changes after the config is loaded", ) +@click.option("--missing-only", is_flag=True) @click.argument("git_log_args", nargs=-1, type=click.UNPROCESSED) -def regen(keys, stash, git_log_args): +def regen(keys, stash, missing_only, git_log_args): """ Regenerate stats for all commits matching git log args (using the config as it existed pre-stash) @@ -140,24 +169,27 @@ def regen(keys, stash, git_log_args): .strip() ) - args = ["git", "log", "--format=%H"] - if git_log_args: - args.extend(git_log_args) - - try: - output = subprocess.check_output(args).decode("utf-8") - except subprocess.CalledProcessError as e: - exit(e.returncode) - - commits = output.splitlines() - config = Config.load_yaml() if not keys: keys = config.stats_keys() - if not click.prompt( - f"Regenerate {list(keys)} stats for {len(commits)} commits?", default=True - ): + stats = Stats().load( + keys=keys, config=config, fill_defaults=False, git_log_args=list(git_log_args) + ) + + commits = stats.commits + + if missing_only: + total_commits = len(commits) + commits = stats.commits_missing_stats(keys) + prompt = ( + f"Regenerate {list(keys)} stats for {len(commits)}" + + f" of {total_commits} commits?" + ) + else: + prompt = f"Regenerate {list(keys)} stats for {len(commits)} commits?" + + if not click.prompt(prompt, default=True): exit(1) if stash: @@ -184,6 +216,10 @@ def regen(keys, stash, git_log_args): ) for key in keys: + if missing_only and stats.commit_has_stat(commit, key): + # Can skip this... + continue + command = config.command_for_stat(key) value = ( subprocess.check_output(command, shell=True) @@ -203,7 +239,14 @@ def regen(keys, stash, git_log_args): @cli.command() -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) @click.option("--commitish", default="HEAD") def delete(keys, commitish): """Delete a single stat""" @@ -237,11 +280,23 @@ def clear(remote): @cli.command() -@click.option("--key", "-k", "keys", default=[], multiple=True) +@click.option( + "--key", + "-k", + "keys", + default=[], + multiple=True, + help="Stats to include (all if not specified)", +) +@click.option( + "--regen-missing/--no-regen-missing", + default=True, + help="Regenerate stats on commits missing them", +) @click.option("--git-name", default="github-actions") @click.option("--git-email", default="github-actions@github.com") @click.pass_context -def ci(ctx, keys, git_name, git_email): +def ci(ctx, keys, regen_missing, git_name, git_email): """All-in-one fetch, save, push""" # TODO pass these to save instead of setting config? click.secho("Setting git user.name and user.email...", fg="cyan") @@ -255,9 +310,15 @@ def ci(ctx, keys, git_name, git_email): click.secho("Fetching stats from remote...", fg="cyan") ctx.invoke(fetch) - click.secho("\nSaving stats...", fg="cyan") + click.secho("\nSaving stats for the current commit...", fg="cyan") ctx.invoke(save, keys=keys) + if regen_missing: + click.secho( + "\nRegenerating stats for last 10 commits if they are missing...", fg="cyan" + ) + ctx.invoke(regen, keys=keys, missing_only=True, git_log_args=["-n", "10"]) + click.secho("\nPushing stats to remote...", fg="cyan") ctx.invoke(push) diff --git a/commitstat/core.py b/commitstat/core.py index d07d35b..eb8554b 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -121,7 +121,7 @@ def show(self, commitish="HEAD"): ["git", "notes", "--ref", self.stats_ref, "show", commitish] ) - def log(self, *, keys, config, values_only, fmt, git_log_args=[]): + def load(self, *, keys, config, fill_defaults, git_log_args=[]): stats = CommitStats() output = subprocess.check_output( @@ -138,35 +138,52 @@ def log(self, *, keys, config, values_only, fmt, git_log_args=[]): for line in output.splitlines(): if commit is None: + # We are expecting a new commit hash commit = line stats.append_commit(commit) - # Fill empty values first for all the keys we expect - for key in keys: - stats.add( - commit=commit, - key=key, - value=config.default_for_stat(key), - type=config.type_for_stat(key), - ) + + if fill_defaults: + # Fill empty values first for all the keys we expect + for key in keys: + stats.add_or_update( + commit=commit, + key=key, + value=config.default_for_stat(key), + type=config.type_for_stat(key), + ) + continue if line == "COMMIT": + # The next line will be the commit hash commit = None continue if not line.strip(): + # Skip empty lines continue + # Parse a stat! key, value = line.split(":", 1) if key in keys: - stats.add( + stats.add_or_update( commit=commit, key=key, value=value, type=config.type_for_stat(key), ) + return stats + + def log(self, *, keys, config, values_only, fmt, git_log_args=[]): + stats = self.load( + keys=keys, + config=config, + fill_defaults=True, + git_log_args=git_log_args, + ) + if fmt == "tsv": stats.print(values_only=values_only, sep="\t") elif fmt == "csv": diff --git a/commitstat/stats.py b/commitstat/stats.py index 7e9db32..65874c8 100644 --- a/commitstat/stats.py +++ b/commitstat/stats.py @@ -12,8 +12,8 @@ def __init__(self): def append_commit(self, commit): self.commits.append(commit) - def add(self, *, commit, key, value, type): - self.stats.setdefault(key, CommitStat(type=type)).add(commit, value) + def add_or_update(self, *, commit, key, value, type): + self.stats.setdefault(key, CommitStat(type=type)).add_or_update(commit, value) def print(self, values_only=False, sep="\t"): if not values_only: @@ -40,6 +40,26 @@ def sparklines(self): print(s) print() + def get(self, key, commit): + return self.stats[key].get(commit) + + def commit_has_stat(self, commit, key): + key_exists = key in self.stats + commit_has_value = commit in self.stats[key].commit_values + return key_exists and commit_has_value + + def commits_missing_stats(self, keys): + """Get a list of commits where any of the given stat keys are missing""" + commits = [] + + for commit in self.commits: + for key in keys: + if not self.commit_has_stat(commit, key): + commits.append(commit) + break + + return commits + class CommitStat: """A single stat for multiple commits, stored by commit sha""" @@ -64,7 +84,7 @@ def parse_value(self, value): return value - def add(self, commit, value): + def add_or_update(self, commit, value): self.commit_values[commit] = self.parse_value(value) def get(self, commit): From 502fe235f2c3a79c4d4f628dee992519f1d93721 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Wed, 11 Jan 2023 23:09:11 -0600 Subject: [PATCH 19/23] Skip regen prompt if isatty --- commitstat/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 70f9e54..01666cf 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -1,3 +1,5 @@ +import os +import sys import subprocess import click from .core import Stats @@ -189,7 +191,8 @@ def regen(keys, stash, missing_only, git_log_args): else: prompt = f"Regenerate {list(keys)} stats for {len(commits)} commits?" - if not click.prompt(prompt, default=True): + # Let CI skip this if it can't prompt + if not os.isatty(sys.stdin.fileno()) or not click.prompt(prompt, default=True): exit(1) if stash: From db391fcf3f5ae63bb7ca4d36f0852d1f6d91e37a Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Wed, 11 Jan 2023 23:12:24 -0600 Subject: [PATCH 20/23] Fix regen prompt --- commitstat/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commitstat/cli.py b/commitstat/cli.py index 01666cf..6c6fa7c 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -192,8 +192,8 @@ def regen(keys, stash, missing_only, git_log_args): prompt = f"Regenerate {list(keys)} stats for {len(commits)} commits?" # Let CI skip this if it can't prompt - if not os.isatty(sys.stdin.fileno()) or not click.prompt(prompt, default=True): - exit(1) + if os.isatty(sys.stdin.fileno()): + click.confirm(prompt, abort=True) if stash: subprocess.check_call(["git", "stash"], stdout=subprocess.DEVNULL) From 0e7d50c5578608bbca27dc0cbb602c5cac094983 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Wed, 11 Jan 2023 23:18:01 -0600 Subject: [PATCH 21/23] Exit regen if no missing stats --- commitstat/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commitstat/cli.py b/commitstat/cli.py index 6c6fa7c..5de76b7 100644 --- a/commitstat/cli.py +++ b/commitstat/cli.py @@ -184,6 +184,9 @@ def regen(keys, stash, missing_only, git_log_args): if missing_only: total_commits = len(commits) commits = stats.commits_missing_stats(keys) + if not commits: + click.secho("No missing stats!", fg="green") + return prompt = ( f"Regenerate {list(keys)} stats for {len(commits)}" + f" of {total_commits} commits?" From 121eeac1f4fb5ce95a5524f4a6a75580fa3d4d81 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Wed, 11 Jan 2023 23:36:13 -0600 Subject: [PATCH 22/23] Fetch depth notes --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 245eeac..4e4e7de 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,54 @@ jobs: ``` +By default, `ci` will try to also regenerate any missing stats for the latest 10 commits (useful if you push multiple commits at once). + +But in order for this to work, you need more commits accessible in CI (TODO can you log off the git remote?). In GitHub Actions, you can use `fetch-depth` to do this: + +```yaml +name: test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 10 + # ... + - run: pipx run --spec git+https://github.com/dropseed/commitstat@git-stats git-stats ci +``` + +You can also send the stats to the GitHub Actions summary: + +```yaml +name: test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 10 + # ... + - run: | + pipx install git+https://github.com/dropseed/commitstat@git-stats + git stats ci + + echo "## Commit Stats" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + git stats log --format sparklines --reverse >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + +``` + ## Retroactive stats You're probably introducing this into an existing project, From 1cd6041bbd415f06ae59443fbd0527bdc0f7c0d9 Mon Sep 17 00:00:00 2001 From: Dave Gaeddert Date: Tue, 24 Jan 2023 21:46:31 -0600 Subject: [PATCH 23/23] Set unparseable value as missing --- commitstat/core.py | 3 +++ commitstat/stats.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/commitstat/core.py b/commitstat/core.py index eb8554b..fcbccb6 100644 --- a/commitstat/core.py +++ b/commitstat/core.py @@ -10,6 +10,9 @@ def __init__(self): def save(self, *, key, value, commitish="HEAD"): # TODO use the new CommitStats classes for this? # would be nice to keep existing order for stats though in message + + # TODO should verify value is valid -- re-use parse_value on CommitStat? + # don't want to save a malformed value stat_line = f"{key}: {value}" if existing_stats := self.get(commitish): diff --git a/commitstat/stats.py b/commitstat/stats.py index 65874c8..8051557 100644 --- a/commitstat/stats.py +++ b/commitstat/stats.py @@ -85,7 +85,13 @@ def parse_value(self, value): return value def add_or_update(self, commit, value): - self.commit_values[commit] = self.parse_value(value) + try: + self.commit_values[commit] = self.parse_value(value) + except ValueError: + print( + f'Could not parse value "{value}" on commit {commit}. ' + + 'Setting value to "missing".' + ) def get(self, commit): return self.commit_values[commit]