diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 28b3725..f34fc64 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,12 +11,12 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.18 + go-version: "1.20" - name: Build run: make @@ -28,7 +28,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout the source repository + # https://github.com/actions/checkout + uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 @@ -37,7 +39,7 @@ jobs: run: git fetch --tags origin main - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: # Integration testing with coverage requires >=1.20 go-version: ">=1.20" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1377c70 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +on: + release: + types: [created] +name: Handle Release +jobs: + generate: + name: Create release-artifacts + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Install upx + run: sudo apt install upx + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ">=1.20" + - name: Generate the artifacts + run: make release + - name: Upload the artifacts + uses: skx/github-action-publish-binaries@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: 'build/git-spend*' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 04b2b74..3ee5810 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Build build/* ^build/README*.md +.flatpak-builder # Tests test-coverage diff --git a/LICENSE b/LICENSE index 42ef316..30f6190 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,8 @@ -Copyright 2023 Antoine Goutenoir +Copyright 2023 THE SOFTWARE MAY NOT BE USED AS AN OPPRESSION TOOL Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/Makefile b/Makefile index 303099b..ffea569 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,10 @@ VERSION=$(shell git describe --tags) -BINARY_PATH=build/gitime +BINARY_PATH=build/git-spend SOURCE=. +# /!. CAREFUL: THIS DIRECTORY WILL BE RM -RF 'ed +TMP_MAN_PATH=/tmp/git-spend-man # Use the -s and -w linker flags to strip the debugging information LD_FLAGS_STRIP=-s -w @@ -20,53 +22,70 @@ depend: go get run: - go run main.go + go run . sum: - go run main.go sum + go run . sum clean: - rm -f $(BINARY_PATH) - rm -f $(BINARY_PATH)-coverage - rm -f $(BINARY_PATH).exe + rm -f "$(BINARY_PATH)" + rm -f "$(BINARY_PATH).upx" + rm -f "$(BINARY_PATH)-coverage" + rm -f "$(BINARY_PATH).exe" + rm -f "$(BINARY_PATH).000" rm -f test-coverage/* + rm -rf "$(TMP_MAN_PATH)" build:# $(shell find . -name \*.go) go build -ldflags="$(LD_FLAGS_STRIP)" -o $(BINARY_PATH) $(SOURCE) build-coverage: go build -cover -o $(BINARY_PATH)-coverage $(SOURCE) -# -coverpkg github.com/goutte/gitime,github.com/goutte/gitime/gitime,github.com/goutte/gitime/cmd \ + +build-linux-arm64: $(shell find . -name \*.go) + GOOS=windows GOARCH=arm64 go build -ldflags="$(LD_FLAGS_STRIP)" -o $(BINARY_PATH).arm64 $(SOURCE) build-windows-amd64: $(shell find . -name \*.go) GOOS=windows GOARCH=amd64 go build -ldflags="$(LD_FLAGS_STRIP)" -o $(BINARY_PATH).exe $(SOURCE) -release: clean build build-windows-amd64 +release: clean build build-windows-amd64 build-linux-arm64 upx --ultra-brute $(BINARY_PATH) upx --ultra-brute $(BINARY_PATH).exe test: test-unit -test-all: test-depend test-unit test-acceptance +test-all: test-unit test-acceptance test-unit: go test `go list ./...` -test-acceptance: build +test-acceptance: build test-acceptance-depend test/bats/bin/bats test -test-depend: +test-acceptance-depend: git submodule update --init --recursive coverage: go test `go list ./...` -coverprofile=coverage-unit.txt -covermode=atomic coverage-acceptance: clean build-coverage - GITIME_COVERAGE=1 test/bats/bin/bats test + GIT_SPEND_COVERAGE=1 test/bats/bin/bats test go tool covdata textfmt -i=test-coverage/ -o coverage-integration.txt install: build - sudo install build/gitime /usr/local/bin/ + sudo install "$(BINARY_PATH)" /usr/local/bin/ install-release: release - sudo install build/gitime /usr/local/bin/ + sudo install "$(BINARY_PATH)" /usr/local/bin/ + +man: clean + mkdir -p "$(TMP_MAN_PATH)" + go run . man --output "$(TMP_MAN_PATH)" + echo "man pages were generated in $(TMP_MAN_PATH)" + +install-man: build + #sudo go run . man --install # nope, `go` may not be available to `root` + sudo "$(BINARY_PATH)" man --install + # … same as + #sudo mkdir -p /usr/local/share/man/man8 + #sudo install "$(TMP_MAN_PATH)"/git-spend*.8 /usr/local/share/man/man8/ diff --git a/README.md b/README.md index c6efbc6..aecd717 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -gitime : time tracker using git commit message commands +git-spend : time tracker using git commit message commands ======================================================= -[![MIT](https://img.shields.io/github/license/Goutte/gitime?style=for-the-badge)](LICENSE) -[![Release](https://img.shields.io/github/v/release/Goutte/gitime?include_prereleases&style=for-the-badge)](https://github.com/Goutte/gitime/releases) -[![Build Status](https://img.shields.io/github/actions/workflow/status/Goutte/gitime/go.yml?style=for-the-badge)](https://github.com/Goutte/gitime/actions/workflows/go.yml) -[![Coverage](https://img.shields.io/codecov/c/github/Goutte/gitime?style=for-the-badge)](https://app.codecov.io/gh/Goutte/gitime/) -[![A+](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/Goutte/gitime) -[![Code Quality](https://img.shields.io/codefactor/grade/github/Goutte/gitime?style=for-the-badge)](https://www.codefactor.io/repository/github/Goutte/gitime) -[![Download](https://img.shields.io/github/downloads/Goutte/gitime/total?style=for-the-badge)](https://github.com/Goutte/gitime/releases/latest/download/gitime) +[![MIT](https://img.shields.io/github/license/Goutte/git-spend?style=for-the-badge)](LICENSE) +[![Release](https://img.shields.io/github/v/release/Goutte/git-spend?include_prereleases&style=for-the-badge)](https://github.com/Goutte/git-spend/releases) +[![Build Status](https://img.shields.io/github/actions/workflow/status/Goutte/git-spend/go.yml?style=for-the-badge)](https://github.com/Goutte/git-spend/actions/workflows/go.yml) +[![Coverage](https://img.shields.io/codecov/c/github/Goutte/git-spend?style=for-the-badge)](https://app.codecov.io/gh/Goutte/git-spend/) +[![A+](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=for-the-badge)](https://goreportcard.com/report/github.com/Goutte/git-spend) +[![Code Quality](https://img.shields.io/codefactor/grade/github/Goutte/git-spend?style=for-the-badge)](https://www.codefactor.io/repository/github/Goutte/git-spend) +[![Download](https://img.shields.io/github/downloads/Goutte/git-spend/total?style=for-the-badge)](https://github.com/Goutte/git-spend/releases/latest/download/git-spend) Purpose @@ -15,12 +15,18 @@ Purpose Collect, addition and return all the `/spend` and `/spent` time-tracking directives in git commit messages. -> This only looks at the `git log` of the currently checked out branch. +> This looks at the `git log` of the currently checked out branch of the working directory, +> and therefore requires `git` to be installed on your system. -**TLDR; JUST [DOWNLOAD LINUX/MAC](https://github.com/Goutte/gitime/releases/latest/download/gitime) — [DOWNLOAD WINDOWS](https://github.com/Goutte/gitime/releases/latest/download/gitime.exe)** +**TLDR; JUST [DOWNLOAD LINUX/MAC] — [DOWNLOAD WINDOWS]** +[DOWNLOAD LINUX/MAC]: https://github.com/Goutte/git-spend/releases/latest/download/git-spend +[DOWNLOAD WINDOWS]: https://github.com/Goutte/git-spend/releases/latest/download/git-spend.exe -### Example of a parsed commit + +### By Example + +Say you are in the directory of a project with one commit like so : ``` feat(crunch): implement a nice feature @@ -31,13 +37,14 @@ Careful, it's still sharp. Running: ``` -$ gitime sum +$ git spend sum ``` would yield: > `1 day 2 hours 30 minutes` -Of course, _gitime_ really shines when you have multiple commits with `/spend` commands that you want to sum. +Of course, _git-spend_ really shines when you have multiple commits with `/spend` commands that you want to tally and sum. +> 💡 You can use `git-spend sum` or `git spend sum`, they are equivalent. ### Specifications @@ -53,26 +60,42 @@ The [acceptance testing suite](./test/features.bats) also holds many usage examp Usage ----- -Go into your git-versioned project's directory, and run: +Go into your git-versioned project's directory: ``` cd -gitime sum +``` + +and run: + +``` +git spend sum +``` +> `2 days 1 hour 42 minutes` + +Or run `git-spend` from anywhere, but specify the `--target` directory (which defaults to `.`): + +``` +git spend sum --target ``` > `2 days 1 hour 42 minutes` +> ⛑ Use `git spend sum --help` or `man git-spend-sum` to see all the options. +> Meanwhile, let's look at some available options, below. + + ### Format the output -You can also get the spent time in a specific unit : +You can get the spent time in a specific unit : ``` -gitime sum --minutes -gitime sum --hours -gitime sum --days +git spend sum --minutes +git spend sum --hours +git spend sum --days ``` > These values will always be rounded to integers, for convenience, -> although _gitime_ does understand floating point numbers in `/spend` directives. +> although _git-spend_ does understand floating point numbers in `/spend` directives. ### Filter by commit authors @@ -80,7 +103,7 @@ gitime sum --days You can track the time of specified authors only, by `name` or `email` : ``` -gitime sum --author Alice --author bob@email.net +git spend sum --author Alice --author bob@email.net ``` @@ -89,54 +112,74 @@ gitime sum --author Alice --author bob@email.net You can also exclude merge commits : ``` -gitime sum --no-merges +git spend sum --no-merges ``` ### Restrict to a range of commits -You can, also restrict to a range of commits, using a commit hash, a tag, or even `HEAD~5`. +You can restrict to a range of commits, using a commit hash, a tag, or even `HEAD~N`. ``` -gitime sum --since --until +git spend sum --since --until ``` -For example, to get the time spent on the last 15 commits : +For example, to get the time spent on the last `15` commits : ``` -gitime sum --since HEAD~15 +git spend sum --since HEAD~15 ``` Or the time spent on a tag since previous tag : ``` -gitime sum --since 0.1.0 --until 0.1.1 +git spend sum --since 0.1.0 --until 0.1.1 ``` You can also use _dates_ and _datetimes_, but remember to quote them if you specify the time: ``` -gitime sum --since "21-03-2023 13:37:00" +git spend sum --since 2023-03-21 +git spend sum --since "2023-03-21 13:37:00" ``` -> Other supported time formats: `RFC3339`, `RFC822`, `RFC850`, +> 📅 Other supported time formats: [`RFC3339`], [`RFC822`], [`RFC850`]. +> If you need a specific timezone, try setting the `TZ` environment variable: +> `TZ="Europe/Paris" git-spend sum --since 2023-03-21` + +[`RFC3339`]: https://www.rfc-editor.org/rfc/rfc3339 +[`RFC822`]: https://www.w3.org/Protocols/rfc822/ +[`RFC850`]: https://www.rfc-editor.org/rfc/rfc850 Download -------- -You can [download the binary](https://github.com/Goutte/gitime/releases/latest/download/gitime) straight from the [latest build in the releases](https://github.com/Goutte/gitime/releases). +### Direct download + +You can [⮋ download the binary](https://github.com/Goutte/git-spend/releases/latest/download/git-spend) straight from the [latest build in the releases](https://github.com/Goutte/git-spend/releases), +and move it anywhere in your `$PATH`, such as `/usr/local/bin/git-spend` for example. + +> ⚠ Remember to enable the execution bit with `chmod u+x ./git-spend`, for example. + +There is an _experimental_ install script that does exactly this, plus `man` pages generation: + + curl https://raw.githubusercontent.com/Goutte/git-spend/main/install.sh | sh + +> 🐧 This script only works for `linux/amd64`, for now. _Stigmergy?_ + +### Via `go get` You can also install via `go get` (hopefully) : ``` -go get -u github.com/goutte/gitime +go get -u github.com/goutte/git-spend ``` or `go install`: ``` -go install github.com/goutte/gitime +go install github.com/goutte/git-spend ``` > If that fails, you can install by cloning and running `make install`. @@ -145,15 +188,18 @@ go install github.com/goutte/gitime Advanced Usage -------------- -### Read from stdin +### Read from standard input -You can also parse messages from `stdin` instead of the git log: +You can also directly parse messages from `stdin` +instead of attempting to read the git log: ``` git log > git.log -cat git.log | gitime sum +cat git.log | git-spend sum --stdin ``` +> `git spend` ignores standard input otherwise. + ### Configure the time modulo @@ -162,25 +208,37 @@ in order to mitigate labor oppression tactics from monopoly hoarders, you can use environment variables to control how time is "rolled over" between units : ``` -GITIME_HOURS_IN_ONE_DAY=7 gitime sum +GIT_SPEND_HOURS_PER_DAY=7 git-spend sum ``` Here are the available environment variables : -- `GITIME_MINUTES_IN_ONE_HOUR` (default: `60`) -- `GITIME_HOURS_IN_ONE_DAY` (default: `8`) -- `GITIME_DAYS_IN_ONE_WEEK` (default: `5`) -- `GITIME_WEEKS_IN_ONE_MONTH` (default: `4`) +- `GIT_SPEND_MINUTES_PER_HOUR` (default: `60`) +- `GIT_SPEND_HOURS_PER_DAY` (default: `8`) +- `GIT_SPEND_DAYS_PER_WEEK` (default: `5`) +- `GIT_SPEND_WEEKS_PER_MONTH` (default: `4`) + + +### Install the man pages + +If you installed via direct download, you might want to install the `man` pages: + +``` +sudo git spend man --install +``` +> `git help spend` will then work as expected. Develop ------- +First, you'll need to [install Golang](https://go.dev/dl/). + ``` -git clone https://github.com/Goutte/gitime.git -cd gitime +git clone https://github.com/Goutte/git-spend.git +cd git-spend go get go run main.go ``` @@ -189,21 +247,30 @@ go run main.go Build & Run & Install --------------------- +The binaries in the releases are built by our [Continuous Integration](./.github/workflows/release.yml). + +Nevertheless, if you want to build your own `git-spend`, you can clone this project and run: + ``` make -make sum make install ``` -> `upx` is used to reduce the binary size in `make install-release`. +> [`upx`] is used to reduce the binary size in `make install-release`. + +[`upx`]: https://upx.github.io/ + +--- + +You can compare the checksums, and they should be the same unless microsoft is being naughty. ### Build for other platforms -You may use the `GOAS` and `GOARCH` environment variables to control the build targets: +You may use the `GOOS` and `GOARCH` environment variables to control the build targets: ``` -GOOS= GOARCH= go build -o build/gitime . +GOOS= GOARCH= go build -o build/git-spend . ``` To list available targets (`os`/`arch`), you can run: @@ -212,22 +279,38 @@ To list available targets (`os`/`arch`), you can run: go tool dist list ``` -> There's an example in the `Makefile`, with the recipe `make build-windows-amd64`. +> There's an example in the [`Makefile`], with the recipe `make build-windows-amd64`. +[`Makefile`]: ./Makefile Contribute ---------- Merge requests are welcome. Make sure you record the time you `/spend` in your commit messages. :) +### Translations + +Translations files are in `locale/*.toml`. +To add another language, add a new file, some sugar, some water, and … _voilà !_ ### Ideas Stash > You can pick and start any, or do something else entirely. - -- [ ] `curl install.sh | bash` -- [ ] flatpak -- [ ] git extension -- [ ] docker -- [ ] i18n _(godspeed)_ -- [ ] Right-To-Left _(help)_ +> If you don't like any of these, please voice your concerns as early as possible. + +- [x] `curl install.sh | sudo sh` _(ongoing, wider support needed)_ +- [x] i18n _(ongoing, [cobra forked](https://github.com/Goutte/cobra/tree/feat-i18n))_ +- [ ] `git-spend sum --format ` +- [ ] `git-spend sum --short` → `1d3h27m` +- [ ] `git-spend chrono start` → start an internal chronometer +- [ ] `git-spend chrono add` → add time to the chronometer +- [ ] Rewriting of `/spend chrono [± ]` by commit hook +- [ ] Rewriting of `/spend ]` by commit hook 🌟 +- [ ] `git spend hook --install` to install git hooks for rewriting +- [ ] `git spend hook --remove` to remove installed git hooks +- [ ] `git-spend amend ` → amend previous commit with `/spend ` +- [ ] `git-spend amend --add ` → same but adds +- [ ] `git-spend amend --subtract ` → same but subtracts (alias: `--sub` ?) +- [ ] docker _(`docker run git-spend` -- awkward? ; would need a volume)_ +- [ ] flatpak perhaps (road blocked, see [`packaging/`](./packaging)) +- [ ] Right-To-Left _(ساعد)_ diff --git a/build/README.md b/build/README.md index b1dc716..0e8cd72 100644 --- a/build/README.md +++ b/build/README.md @@ -1 +1 @@ -Your build files (`gitime`) will appear here when you `make`. \ No newline at end of file +Your build files (`git-spend`) will appear here when you `make`. \ No newline at end of file diff --git a/cmd/man.go b/cmd/man.go new file mode 100644 index 0000000..cb10e9a --- /dev/null +++ b/cmd/man.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "fmt" + "github.com/goutte/git-spend/locale" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + "log" + "os" +) + +// manSection is to be determined/discussed +const manSection = 8 + +// manPath is injected with manSection, and holds where the man pages are installed +const manPath = "/usr/local/share/man/man%d/" + +var ( + FlagOutput string + FlagInstall bool +) + +// manCmd generates manpage(s) for git-spend +var manCmd = &cobra.Command{ + Hidden: true, + Use: "man", + Short: locale.T("CommandManSummary"), + Long: locale.T("CommandManDescription"), + Run: func(cmd *cobra.Command, args []string) { + header := &doc.GenManHeader{ + Title: "git-spend", + Section: fmt.Sprintf("%d", manSection), + Manual: "⌛", + Source: "git-spend man", + } + + outputDir := FlagOutput + if FlagInstall && outputDir == "." { + outputDir = fmt.Sprintf(manPath, manSection) + } + + err := os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + log.Fatal(err) + } + + err = doc.GenManTree(rootCmd, header, outputDir) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + rootCmd.AddCommand(manCmd) + manCmd.Flags().StringVar( + &FlagOutput, + "output", + ".", + locale.T("CommandManFlagOutput"), + ) + manCmd.Flags().BoolVar( + &FlagInstall, + "install", + false, + locale.T("CommandManFlagInstall"), + ) +} diff --git a/cmd/root.go b/cmd/root.go index df60050..cb6efad 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,9 @@ package cmd import ( - "github.com/goutte/gitime/gitime" + "fmt" + "github.com/goutte/git-spend/gitime" + "github.com/goutte/git-spend/locale" "os" "github.com/spf13/cobra" @@ -10,9 +12,10 @@ import ( var ( rootCmd = &cobra.Command{ - Use: "gitime", - Short: "Sum up your /spent time on commits", - Long: `Gather information about /spent time from commit messages.`, + Use: "git-spend", + Short: locale.T("CommandRootSummary"), + Long: locale.T("CommandRootDescription"), + DisableAutoGenTag: true, } ) @@ -22,34 +25,48 @@ func Execute() error { } func init() { - cobra.OnInitialize(initConfig) + // If we want the generated help to show correct defaults, we need this BEFORE cobra inits + initConfig() + + // We will need to use a listener like so when we'll use a flag for the config file + //cobra.OnInitialize(initCobra) // Might use a config file as well at some point for things like DaysInOneWeek - //rootCmd.PersistentFlags().StringVar(&configFileFlag, "config", "", "config file (default is $HOME/.gitime.yaml)") + //rootCmd.PersistentFlags().StringVar(&configFileFlag, "config", "", "config file (default is $HOME/.git-spend.yaml)") // Snippets for viper config //rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution") //viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author")) - //viper.SetDefault("author", "NAME HERE ") } func initConfig() { // Later on we'll want users to be able to override the config file // But first we need to figure out how to generate a "template" for that config file. - //if configFileFlag != "" { - // viper.SetConfigFile(configFileFlag) + //if configFile != "" { + // viper.SetConfigFile(configFile) //} else { home, err := os.UserHomeDir() - cobra.CheckErr(err) - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".gitime") + if err != nil { + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".git-spend") + } //} - viper.SetEnvPrefix("gitime") + viper.SetEnvPrefix("git_spend") viper.AutomaticEnv() _ = viper.ReadInConfig() gitime.UpdateTimeModuloConfiguration() } + +func fail(anything interface{}, command *cobra.Command) { + // Questions: + // - stderr ? + // - CLI colors ? + fmt.Println(anything) + + _ = command.Help() + os.Exit(1) +} diff --git a/cmd/sum.go b/cmd/sum.go index ec5c8a4..cc78f8c 100644 --- a/cmd/sum.go +++ b/cmd/sum.go @@ -2,76 +2,47 @@ package cmd import ( "fmt" - "github.com/goutte/gitime/gitime" + "github.com/goutte/git-spend/gitime" + "github.com/goutte/git-spend/gitime/reader" + "github.com/goutte/git-spend/locale" "github.com/spf13/cobra" - "github.com/tsuyoshiwada/go-gitlog" - "io" - "log" - "os" - "time" + "strings" +) + +const ( + FlagTargetDefault = "." ) var ( - FlagAuthors []string - FlagSince string - FlagUntil string - FlagMinutes bool - FlagHours bool - FlagDays bool - FlagWeeks bool - FlagMonths bool - FlagExcludeMerge bool + FlagAuthors []string + FlagTarget string + FlagStdin bool + FlagSince string + FlagUntil string + FlagMinutes bool + FlagHours bool + FlagDays bool + FlagWeeks bool + FlagMonths bool + FlagNoMerges bool ) var sumCmd = &cobra.Command{ - Use: "sum", - Short: "Sum /spent time recorded in commit messages", - Long: `The /spend and /spent directives will be parsed and summed -from the commit messages of the currently checked out branch -of the git repository of the current working directory. - -You can also get a raw number in a specific unit: - - gitime sum --minutes - -You can also restrict to some commit authors, by name or email: - - gitime sum --author=Alice --author=bob@pop.net --author=Eve - -`, + Use: "sum", + Short: locale.T("CommandSumSummary"), + Long: locale.T("CommandSumDescription"), + DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - ts := Sum(FlagAuthors, FlagExcludeMerge).Normalize() - fmt.Println(formatTimeSpent(ts)) + ts, err := Sum() + if err != nil { + fail(err, cmd) + } + if ts != nil { + fmt.Println(formatTimeSpent(ts.Normalize())) + } }, } -func doesStdinHaveData() bool { - fileInfo, err := os.Stdin.Stat() - if err != nil { - return false - } - - if os.Getenv("GITIME_NO_STDIN") == "1" { - return false - } - - // Both yield false positives in CI, careful - //if (fileInfo.Mode() & os.ModeCharDevice) == 0 { // alternatively? - if (fileInfo.Mode() & os.ModeNamedPipe) != 0 { - return true - } - - return false -} - -func readStdin() string { - stdin, err := io.ReadAll(os.Stdin) - if err != nil { - log.Fatal(err) - } - return fmt.Sprintf("%s", stdin) -} - func formatTimeSpent(ts *gitime.TimeSpent) string { out := "" if FlagMinutes { @@ -88,169 +59,52 @@ func formatTimeSpent(ts *gitime.TimeSpent) string { out = ts.String() } if out == "" { - out = "No time-tracking directives /spend or /spent found in commits." - } - return out -} - -func parseTimePerhaps(input string) *time.Time { - if input == "" { - return nil - } - - layouts := []string{ - time.RFC3339, - time.DateTime, - time.DateOnly, - time.RFC822, - time.RFC850, - } - - for _, layout := range layouts { - parse, err := time.Parse(layout, input) - if err == nil { - return &parse + out = locale.T("CommandSumFailureNothingFound") + if len(FlagAuthors) > 0 { + out += " " + locale.Tf("CommandSumFailureNothingFoundForAuthors", strings.Join( + FlagAuthors, " "+locale.T("Or")+" ", + )) } - } - - return nil -} - -func getRevArgsFromFlags() gitlog.RevArgs { - var rev gitlog.RevArgs = nil - if FlagSince != "" { - flagSinceDate := parseTimePerhaps(FlagSince) - - if FlagUntil != "" { - flagUntilDate := parseTimePerhaps(FlagUntil) - - if flagUntilDate != nil { - if flagSinceDate != nil { - rev = &gitlog.RevTime{ - Since: *flagSinceDate, - Until: *flagUntilDate, - } - } else { - fmt.Println("you cannot mix dates and refs in --until and --since") - os.Exit(1) - } - } else { - if flagSinceDate != nil { - fmt.Println("you cannot mix dates and refs in --since and --until") - os.Exit(1) - } else { - rev = &gitlog.RevRange{ - New: FlagUntil, - Old: FlagSince, - } - } - } - } else { - if flagSinceDate != nil { - rev = &gitlog.RevTime{ - Since: *flagSinceDate, - } - } else { - rev = &gitlog.RevRange{ - New: "HEAD", - Old: FlagSince, - } - } + if FlagSince != "" { + out += " " + out += locale.Tf("CommandSumFailureNothingFoundAfterSince", FlagSince) } - } else { if FlagUntil != "" { - flagUntilDate := parseTimePerhaps(FlagUntil) - if flagUntilDate != nil { - rev = &gitlog.RevTime{ - Until: *flagUntilDate, - } - } else { - rev = &gitlog.Rev{ - Ref: FlagUntil, - } + if FlagSince != "" { + out += " " + locale.T("And") } - } - } - return rev -} - -// ReadGitLog reads the git log of the repository of the specified directpry -func ReadGitLog(onlyAuthors []string, excludeMerge bool, directory string) string { - git := gitlog.New(&gitlog.Config{ - Path: directory, - }) - rev := getRevArgsFromFlags() - params := &gitlog.Params{ - IgnoreMerges: excludeMerge, - } - commits, err := git.Log(rev, params) - if err != nil { - fmt.Println("Cannot read git log:", err) - os.Exit(1) - //log.Fatalln("Cannot read git log:", err) - } - s := "" - for _, commit := range commits { - if !isCommitByAnyAuthor(commit, onlyAuthors) { - continue + out += " " + locale.Tf("CommandSumFailureNothingFoundBeforeUntil", FlagUntil) } - - s += commit.Subject + "\n" - s += commit.Body + "\n" + out += "." } - - return s + return out } -func Sum(onlyAuthors []string, excludeMerge bool) *gitime.TimeSpent { +func Sum() (*gitime.TimeSpent, error) { var gitLog string - if doesStdinHaveData() { - if len(onlyAuthors) > 0 { - log.Fatalln(`Flag --author is not supported with stdin parsing. -Meanwhile, you can use --author on git log, like so: - - git log --author Bob > log.log && cat log.log | gitime sum`) + if FlagStdin { + if len(FlagAuthors) > 0 { + return nil, fmt.Errorf(locale.T("CommandSumFailureStdinAuthors")) } - if excludeMerge { - log.Fatalln(`Flag --no-merges is not supported with stdin parsing. -Meanwhile, you can use --no-merges on git log, like so: - - git log --no-merges > log.log && cat log.log | gitime sum`) + if FlagNoMerges { + return nil, fmt.Errorf(locale.T("CommandSumFailureStdinNoMerges")) } if FlagSince != "" { - log.Fatalln(`Flag --since is not supported with stdin parsing.`) + return nil, fmt.Errorf(locale.T("CommandSumFailureStdinSince")) } if FlagUntil != "" { - log.Fatalln(`Flag --until is not supported with stdin parsing.`) - } - gitLog = readStdin() - } else { - gitLog = ReadGitLog(onlyAuthors, excludeMerge, ".") - } - - return gitime.CollectTimeSpent(gitLog) -} - -func isCommitByAnyAuthor(commit *gitlog.Commit, authors []string) bool { - if len(authors) == 0 { - return true - } - - if commit.Author == nil { - return false - } - - for _, author := range authors { - if commit.Author.Name == author { - return true + return nil, fmt.Errorf(locale.T("CommandSumFailureStdinUntil")) } - if commit.Author.Email == author { - return true + if FlagTarget != FlagTargetDefault { + return nil, fmt.Errorf(locale.T("CommandSumFailureStdinTarget")) } + gitLog = reader.ReadStdin() + } else { + gitLog = reader.ReadGitLog(FlagAuthors, FlagNoMerges, FlagSince, FlagUntil, FlagTarget) } - return false + return gitime.CollectTimeSpent(gitLog), nil } func addFormatFlags(command *cobra.Command) { @@ -259,35 +113,43 @@ func addFormatFlags(command *cobra.Command) { "minutes", "", false, - "show sum in minutes", + locale.T("CommandSumFlagMinutesHelp"), ) command.Flags().BoolVarP( &FlagHours, "hours", "", false, - "show sum in hours", + locale.Tf("CommandSumFlagHoursHelp", gitime.MinutesInOneHour), ) command.Flags().BoolVarP( &FlagDays, "days", "", false, - "show sum in days", + locale.Tf("CommandSumFlagDaysHelp", gitime.HoursInOneDay), ) command.Flags().BoolVarP( &FlagWeeks, "weeks", "", false, - "show sum in weeks", + locale.Tf("CommandSumFlagWeeksHelp", gitime.DaysInOneWeek), ) command.Flags().BoolVarP( &FlagMonths, "months", "", false, - "show sum in months", + locale.Tf("CommandSumFlagMonthsHelp", gitime.WeeksInOneMonth), + ) + + command.MarkFlagsMutuallyExclusive( + "months", + "weeks", + "days", + "hours", + "minutes", ) } @@ -296,30 +158,47 @@ func addFilterFlags(command *cobra.Command) { &FlagAuthors, "author", []string{}, - "only use commits by these authors (can be repeated)", + locale.T("CommandSumFlagAuthorsHelp"), ) command.Flags().BoolVar( - &FlagExcludeMerge, + &FlagNoMerges, "no-merges", false, - "ignore merge commits", + locale.T("CommandSumFlagNoMergesHelp"), ) command.Flags().StringVar( &FlagSince, "since", "", - "only use commits after this ref (exclusive)", + locale.T("CommandSumFlagSinceHelp"), ) command.Flags().StringVar( &FlagUntil, "until", "", - "only use commits before this ref (inclusive)", + locale.T("CommandSumFlagUntilHelp"), + ) +} + +func addTargetFlags(command *cobra.Command) { + command.Flags().StringVar( + &FlagTarget, + "target", + FlagTargetDefault, + locale.T("CommandSumFlagTargetHelp"), + ) + command.Flags().BoolVar( + &FlagStdin, + "stdin", + false, + locale.T("CommandSumFlagStdinHelp"), ) } func init() { rootCmd.AddCommand(sumCmd) - addFormatFlags(sumCmd) + sumCmd.Flags().SortFlags = false + addTargetFlags(sumCmd) addFilterFlags(sumCmd) + addFormatFlags(sumCmd) } diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..9ab2749 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +git-spend (1.2.0-1) UNRELEASED; urgency=medium + + * Initial release (Closes: TODO) + + -- "Antoine Goutenoir" Sat, 11 Nov 2023 09:45:17 +0100 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..ea599f9 --- /dev/null +++ b/debian/control @@ -0,0 +1,212 @@ +Source: git-spend +Maintainer: Debian Go Packaging Team +Uploaders: "Antoine Goutenoir" +Section: golang +Testsuite: autopkgtest-pkg-go +Priority: optional +Build-Depends: debhelper-compat (= 13), + dh-golang, + golang-any +Standards-Version: 4.6.0 +Vcs-Browser: https://salsa.debian.org/go-team/packages/git-spend +Vcs-Git: https://salsa.debian.org/go-team/packages/git-spend.git +Homepage: https://github.com/Goutte/git-spend +Rules-Requires-Root: no +XS-Go-Import-Path: github.com/Goutte/git-spend + +Package: git-spend +Architecture: any +Depends: ${misc:Depends}, + ${shlibs:Depends} +Built-Using: ${misc:Built-Using} +Description: Sum the time-tracking "/spend" commands of git commit messages. (program) + git-spend : time tracker using git commit message commands + . + Purpose + . + Collect, addition and return all the /spend and /spent time-tracking + directives in git commit messages. + . + | This looks at the git log of the currently checked out branch of the + | working directory, + | and therefore requires git to be installed on your system. + . + By Example + . + Say you are in the directory of a project with one commit like so : + . + feat(crunch): implement a nice feature + . + Careful, it's still sharp. + /spend 10h30 + . + Running: + . + $ git spend sum + . + would yield: + . + | 1 day 2 hours 30 minutes + . + Of course, *git-spend* really shines when you have multiple commits with + /spend commands that you want to tally and sum. + . + | 💡 You can use git-spend sum or git spend sum, they are equivalent. + . + Specifications + . + We assume 8 hours per day, 5 days per week, 4 weeks per month. *(like + Gitlab does)* These can be configured at runtime if needed, using + environment variables. + . + The **complete specification** can be found in the rules + (/gitime/gitime_test_data.yaml) of the test data, and in excruciating + detail in the grammar (/gitime/grammar.go). + . + The acceptance testing suite (/test/features.bats) also holds many usage + examples. + . + Usage + . + Go into your git-versioned project's directory: + . + cd + . + and run: + . + git spend sum + . + | 2 days 1 hour 42 minutes + . + Or run git-spend from anywhere, but specify the --target directory (which + defaults to .): + . + git spend sum --target + . + | 2 days 1 hour 42 minutes + . + | ⛑ Use git spend sum --help or man git-spend-sum to see all the options. + | Meanwhile, let's look at some available options, below. + . + Format the output + . + You can get the spent time in a specific unit : + . + git spend sum --minutes + git spend sum --hours + git spend sum --days + . + | These values will always be rounded to integers, for convenience, + | although *git-spend* does understand floating point numbers in /spend + | directives. + . + Filter by commit authors + . + You can track the time of specified authors only, by name or email : + . + git spend sum --author Alice --author bob@email.net + . + Exclude merge commits + . + You can also exclude merge commits : + . + git spend sum --no-merges + . + Restrict to a range of commits + . + You can restrict to a range of commits, using a commit hash, a tag, or + even HEAD~N. + . + git spend sum --since --until + . + For example, to get the time spent on the last 15 commits : + . + git spend sum --since HEAD~15 + . + Or the time spent on a tag since previous tag : + . + git spend sum --since 0.1.0 --until 0.1.1 + . + You can also use *dates* and *datetimes*, but remember to quote them if + you specify the time: + . + git spend sum --since 2023-03-21 + git spend sum --since "2023-03-21 13:37:00" + . + | 📅 Other supported time formats: RFC3339 (https://www.rfc- + | editor.org/rfc/rfc3339), RFC822 + (https://www.w3.org/Protocols/rfc822/), + | RFC850 (https://www.rfc-editor.org/rfc/rfc850). + | If you need a specific timezone, try setting the TZ environment + | variable: + | TZ="Europe/Paris" git-spend sum --since 2023-03-21 + . + Download + . + Direct download + . + You can ⮋ download the binary (https://github.com/Goutte/git- + spend/releases/latest/download/git-spend) straight from the latest build + in the releases (https://github.com/Goutte/git-spend/releases), and move + it anywhere in your $PATH, such as /usr/local/bin/git-spend for example. + . + | ⚠ Remember to enable the execution bit with chmod u+x ./git-spend, for + | example. + . + There is an *experimental* install script that does exactly this, plus + man pages generation: + . + curl https://raw.githubusercontent.com/Goutte/git-spend/main/install.sh + | sh + . + | 🐧 This script only works for linux/amd64, for now. *Stigmergy?* + . + Via go get + . + You can also install via go get (hopefully) : + . + go get -u github.com/goutte/git-spend + . + or go install: + . + go install github.com/goutte/git-spend + . + | If that fails, you can install by cloning and running make install. + . + Advanced Usage + . + Read from standard input + . + You can also directly parse messages from stdin instead of attempting to + read the git log: + . + git log > git.log + cat git.log | git-spend sum --stdin + . + | git spend ignores standard input otherwise. + . + Configure the time modulo + . + If you live somewhere where work hours per week are limited (to 35 for + example) in order to mitigate labor oppression tactics from monopoly + hoarders, you can use environment variables to control how time is + "rolled over" between units : + . + GIT_SPEND_HOURS_IN_ONE_DAY=7 git-spend sum + . + Here are the available environment variables : + . + * GIT_SPEND_MINUTES_IN_ONE_HOUR (default: 60) + * GIT_SPEND_HOURS_IN_ONE_DAY (default: 8) + * GIT_SPEND_DAYS_IN_ONE_WEEK (default: 5) + * GIT_SPEND_WEEKS_IN_ONE_MONTH (default: 4) + . + Install the man pages + . + If you installed via direct download, you might want to install the man + pages: + . + sudo git spend man --install + . + | git help spend will then work as expected. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..5cc5f05 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: git-spend +Upstream-Contact: "Antoine Goutenoir" +Source: https://github.com/Goutte/git-spend + +Files: * +Copyright: 2023 Antoine Goutenoir +License: MIT + +Files: debian/* +Copyright: 2023 Antoine Goutenoir +License: MIT +Comment: Debian packaging is licensed under the same terms as upstream + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..3d450c2 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,3 @@ +[DEFAULT] +debian-branch = debian/sid +dist = DEP14 diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml new file mode 100644 index 0000000..594e14e --- /dev/null +++ b/debian/gitlab-ci.yml @@ -0,0 +1,6 @@ +# auto-generated, DO NOT MODIFY. +# The authoritative copy of this file lives at: +# https://salsa.debian.org/go-team/infra/pkg-go-tools/blob/master/config/gitlabciyml.go +--- +include: + - https://salsa.debian.org/go-team/infra/pkg-go-tools/-/raw/master/pipeline/test-archive.yml diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..a97e135 --- /dev/null +++ b/debian/rules @@ -0,0 +1,7 @@ +#!/usr/bin/make -f + +%: + dh $@ --builddirectory=_build --buildsystem=golang --with=golang + +override_dh_auto_install: + dh_auto_install -- --no-source diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 0000000..7c248eb --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/Goutte/git-spend/issues +Bug-Submit: https://github.com/Goutte/git-spend/issues/new +Repository: https://github.com/Goutte/git-spend.git +Repository-Browse: https://github.com/Goutte/git-spend diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..2cc207b --- /dev/null +++ b/debian/watch @@ -0,0 +1,4 @@ +version=4 +opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%@PACKAGE@-$1.tar.gz%,\ + uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/$1~$2$3/" \ + https://github.com/Goutte/git-spend/tags .*/v?(\d\S*)\.tar\.gz debian diff --git a/docs/README.en.md b/docs/README.en.md new file mode 100644 index 0000000..5d6fbb4 --- /dev/null +++ b/docs/README.en.md @@ -0,0 +1,46 @@ +## Architectural Decision Records + +### No dashes in subcommand names + +> Dashes in subcommands' names do not play well with `man` generation. + +### Some messages are not translated + +Those are messages handled by Cobra, like the usage flag for `--help` and some titles. + +> I think we should try our best to [fix these upstream in Cobra](https://github.com/spf13/cobra/pull/1944). +> For now we're using our branch `feat-i18n` of Cobra, see `go.mod`. + +### Debian package + +> I think `git-spend` could be rewritten in less than `2 Mio`, in pure `bash`. +> A wise friend told me that no-one enjoys maintaining huge and complex bash scripts. +> So I'm undecided, for now ; I'll let _you_ decide if _git-spend_ is worthy of debian packages. + +### Golang v1.20 and upwards + +Golang `1.20` introduces new tools for: +- code coverage _(we use those a lot already)_ +- i18n _(same)_ + + +## Manpages + +Since `git help sum` will try to fetch a manpage, we're providing one. + +To install `man` pages, use: + + sudo git spend man --install + +> That API is very experimental and is likely to change in the future, which is why it's hidden for now. +> Perhaps it should/will become `git spend man install` ? + +or, if you cloned this source tree: + + make install-man + +> … and now a debian package makes sense ; +> can't find yet if `go get` allows hooks to install manpages. + +We could also perhaps show a message somewhere if we detect that manpages are missing +and provide there the command hint to install them. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1f40f94 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,2 @@ + +- [English](./README.en.md) diff --git a/gitime/gitime.go b/gitime/gitime.go index 569db94..b927fd4 100644 --- a/gitime/gitime.go +++ b/gitime/gitime.go @@ -1,7 +1,6 @@ package gitime import ( - "fmt" "regexp" "strconv" "strings" @@ -18,6 +17,7 @@ var expressions = []*regexp.Regexp{ // If no time unit is specified, minutes are assumed. func CollectTimeSpent(message string) *TimeSpent { ts := &TimeSpent{} + message = strings.ReplaceAll(message, "\r", "\n") lines := strings.Split(message, "\n") for _, line := range lines { @@ -72,12 +72,7 @@ func extractTimeComponent(matches []string, r *regexp.Regexp, component string) componentString = matches[componentIndex] } } - componentFloat, err := strconv.ParseFloat(componentString, 64) - if err != nil { - // this should never happen unless weeksRegex fiddle with and break our regexes - fmt.Println("cannot parse", component, componentString, r.String()) - return 0 - } + componentFloat, _ := strconv.ParseFloat(componentString, 64) return componentFloat } diff --git a/gitime/gitime_test_data.yaml b/gitime/gitime_test_data.yaml index 1fdcc59..14a8830 100644 --- a/gitime/gitime_test_data.yaml +++ b/gitime/gitime_test_data.yaml @@ -128,6 +128,12 @@ collect: expected: minutes: 60 + - rule: Allow alias /spent (/spent 0.5h) + message: | + /spent 0.5h + expected: + minutes: 30 + - rule: Allow full units (/spend 1 hour 10 minutes) message: | /spend 1 hour 10 minutes @@ -201,12 +207,25 @@ collect: expected: minutes: 120 + - rule: Handle Windows carriage returns as newlines + message: "style: main menu fixed\r/spent 0.5h" + expected: + minutes: 30 + - rule: Tolerate missing space typo (/spend3h) message: | /spend3h expected: minutes: 180 + - rule: Tolerate leading whitespaces ( /spend 11m) + message: | + feat: amazing tolerance + /spend 11m + Respecting the tolerance paradox. + expected: + minutes: 11 + - rule: Cumulate multiple /spend directives message: | feat: merging a bunch of things diff --git a/gitime/grammar.go b/gitime/grammar.go index c10e066..4de1d9e 100644 --- a/gitime/grammar.go +++ b/gitime/grammar.go @@ -2,19 +2,20 @@ package gitime import "regexp" -var commandRegex = "^/spen[dt]\\s*" +var commandRegex = "^\\s*/spen[dt]\\s*:?\\s*" var floatRegex = "[0-9]+[.]?[0-9]*|[0-9]*[.]?[0-9]+" // no negative lookahead in regexp, so we hack around it (to ignore datetime suffix) -var minutesRegex = "(?P" + floatRegex + ")\\s*(minutes?|mins?|mi?)?([^-/0-9]|$)" -var hoursRegex = "(?P" + floatRegex + ")\\s*(hours?|ho?)\\s*" -var daysRegex = "(?P" + floatRegex + ")\\s*(days?|da?)\\s*" -var weeksRegex = "(?P" + floatRegex + ")\\s*(weeks?|we?)\\s*" -var monthsRegex = "(?P" + floatRegex + ")\\s*(months?|mo)\\s*" -var miP = "(" + minutesRegex + ")?" -var hoP = "(" + hoursRegex + ")?" -var daP = "(" + daysRegex + ")?" -var weP = "(" + weeksRegex + ")?" -var moP = "(" + monthsRegex + ")?" +// there's also regexp2, but its API needs some more work at the time of this writing +var minutesRegex = "(?P" + floatRegex + ")\\s*(?:minutes?|mins?|mi?)?([^-/0-9]|$)" +var hoursRegex = "(?P" + floatRegex + ")\\s*(?:hours?|ho?)\\s*" +var daysRegex = "(?P" + floatRegex + ")\\s*(?:days?|da?)\\s*" +var weeksRegex = "(?P" + floatRegex + ")\\s*(?:weeks?|we?)\\s*" +var monthsRegex = "(?P" + floatRegex + ")\\s*(?:months?|mo)\\s*" +var miP = "(?:" + minutesRegex + ")?" +var hoP = "(?:" + hoursRegex + ")?" +var daP = "(?:" + daysRegex + ")?" +var weP = "(?:" + weeksRegex + ")?" +var moP = "(?:" + monthsRegex + ")?" var spentAllRegex = regexp.MustCompile(commandRegex + moP + weP + daP + hoP + miP) diff --git a/gitime/reader/clock.go b/gitime/reader/clock.go new file mode 100644 index 0000000..f29ad8e --- /dev/null +++ b/gitime/reader/clock.go @@ -0,0 +1,22 @@ +package reader + +import "time" + +func parseTimePerhaps(input string) *time.Time { + layouts := []string{ + time.RFC3339, + time.DateTime, + time.DateOnly, + time.RFC822, + time.RFC850, + } + + for _, layout := range layouts { + parse, err := time.Parse(layout, input) + if err == nil { + return &parse + } + } + + return nil +} diff --git a/gitime/reader/git_log.go b/gitime/reader/git_log.go new file mode 100644 index 0000000..ed7438c --- /dev/null +++ b/gitime/reader/git_log.go @@ -0,0 +1,125 @@ +package reader + +import ( + "fmt" + "github.com/tsuyoshiwada/go-gitlog" + "os" + "os/exec" +) + +// ReadGitLog reads the git log of the repository of the specified directpry +func ReadGitLog(onlyAuthors []string, excludeMerge bool, since string, until string, directory string) string { + git := gitlog.New(&gitlog.Config{ + Path: directory, + }) + rev := getRevArgsFromFlags(since, until) + params := &gitlog.Params{ + IgnoreMerges: excludeMerge, + } + commits, err := git.Log(rev, params) + if exitError, isExitError := err.(*exec.ExitError); isExitError { + fmt.Println("git command unsuccessful:", err, "—", exitError.Stderr) + os.Exit(exitError.ExitCode()) + } + if err != nil { + fmt.Println("cannot read git log:", err) + os.Exit(1) + } + + s := "" + for _, commit := range commits { + if !isCommitByAnyAuthor(commit, onlyAuthors) { + continue + } + + // We read from the raw body because some newlines are eaten when separating subject an body. + // My non-tech friend commits without separating subject and body, like this: + // > style: something amazing + // > /spent 0.5h + // … and the "/spend 0.5h" ends up at the end of the Subject, without newline. + s += commit.RawBody + "\n" + // We also read from the note, and it might or might not be correct. + s += commit.Note + "\n" + } + + return s +} + +func getRevArgsFromFlags(since string, until string) gitlog.RevArgs { + var rev gitlog.RevArgs = nil + if since != "" { + sinceTime := parseTimePerhaps(since) + + if until != "" { + untilTime := parseTimePerhaps(until) + + if untilTime != nil { + if sinceTime != nil { + rev = &gitlog.RevTime{ + Since: *sinceTime, + Until: *untilTime, + } + } else { + fmt.Println("unsupported mix of dates and refs in --until and --since") + os.Exit(1) + } + } else { + if sinceTime == nil { + rev = &gitlog.RevRange{ + New: until, + Old: since, + } + } else { + fmt.Println("unsupported mix of dates and refs in --since and --until") + os.Exit(1) + } + } + } else { + if sinceTime != nil { + rev = &gitlog.RevTime{ + Since: *sinceTime, + } + } else { + rev = &gitlog.RevRange{ + New: "HEAD", + Old: since, + } + } + } + } else { + if until != "" { + untilDate := parseTimePerhaps(until) + if untilDate != nil { + rev = &gitlog.RevTime{ + Until: *untilDate, + } + } else { + rev = &gitlog.Rev{ + Ref: until, + } + } + } + } + return rev +} + +func isCommitByAnyAuthor(commit *gitlog.Commit, authors []string) bool { + if len(authors) == 0 { + return true + } + + if commit.Author == nil { + return false + } + + for _, author := range authors { + if commit.Author.Name == author { + return true + } + if commit.Author.Email == author { + return true + } + } + + return false +} diff --git a/gitime/reader/stdin.go b/gitime/reader/stdin.go new file mode 100644 index 0000000..d8b35a7 --- /dev/null +++ b/gitime/reader/stdin.go @@ -0,0 +1,12 @@ +package reader + +import ( + "fmt" + "io" + "os" +) + +func ReadStdin() string { + stdin, _ := io.ReadAll(os.Stdin) + return fmt.Sprintf("%s", stdin) +} diff --git a/gitime/time_modulo.go b/gitime/time_modulo.go index 2fa1386..1395a6b 100644 --- a/gitime/time_modulo.go +++ b/gitime/time_modulo.go @@ -10,7 +10,7 @@ These are the time modulo constants that Gitlab uses. These values may be set to another value at runtime using ENV variables, eg: - GITIME_HOURS_IN_ONE_DAY=7 gitime sum + GIT_SPEND_HOURS_IN_ONE_DAY=7 git-spend sum */ @@ -36,13 +36,24 @@ var ( // UpdateTimeModuloConfiguration must be ran AFTER viper has loaded the config file and env func UpdateTimeModuloConfiguration() { - MinutesInOneHour = viper.GetFloat64("minutes_in_one_hour") - HoursInOneDay = viper.GetFloat64("hours_in_one_day") - DaysInOneWeek = viper.GetFloat64("days_in_one_week") - WeeksInOneMonth = viper.GetFloat64("weeks_in_one_month") + MinutesInOneHour = getConfigFloat([]string{"minutes_per_hour", "minutes_in_one_hour"}, DefaultMinutesInOneHour) + HoursInOneDay = getConfigFloat([]string{"hours_per_day", "hours_in_one_day"}, DefaultHoursInOneDay) + DaysInOneWeek = getConfigFloat([]string{"days_per_week", "days_in_one_week"}, DefaultDaysInOneWeek) + WeeksInOneMonth = getConfigFloat([]string{"weeks_per_month", "weeks_in_one_month"}, DefaultWeeksInOneMonth) refreshCompoundConversions() } +func getConfigFloat(keys []string, defaultValue float64) float64 { + for _, key := range keys { + val := viper.GetFloat64(key) + if val != defaultValue { + return val + } + } + + return defaultValue +} + func refreshCompoundConversions() { MinutesInOneDay = MinutesInOneHour * HoursInOneDay MinutesInOneWeek = MinutesInOneHour * HoursInOneDay * DaysInOneWeek @@ -52,7 +63,15 @@ func refreshCompoundConversions() { func init() { refreshCompoundConversions() viper.SetDefault("minutes_in_one_hour", DefaultMinutesInOneHour) + viper.SetDefault("minutes_per_hour", DefaultMinutesInOneHour) viper.SetDefault("hours_in_one_day", DefaultHoursInOneDay) + viper.SetDefault("hours_per_day", DefaultHoursInOneDay) viper.SetDefault("days_in_one_week", DefaultDaysInOneWeek) + viper.SetDefault("days_per_week", DefaultDaysInOneWeek) viper.SetDefault("weeks_in_one_month", DefaultWeeksInOneMonth) + viper.SetDefault("weeks_per_month", DefaultWeeksInOneMonth) + //viper.RegisterAlias("minutes_per_hour", "minutes_in_one_hour") + //viper.RegisterAlias("hours_per_day", "hours_in_one_day") + //viper.RegisterAlias("days_per_week", "days_in_one_week") + //viper.RegisterAlias("weeks_per_month", "weeks_in_one_month") } diff --git a/gitime/time_spent.go b/gitime/time_spent.go index 30eeafc..e5c0ddf 100644 --- a/gitime/time_spent.go +++ b/gitime/time_spent.go @@ -2,6 +2,7 @@ package gitime import ( "fmt" + "github.com/goutte/git-spend/locale" "math" ) @@ -159,80 +160,59 @@ func (ts *TimeSpent) normalizeModuli() *TimeSpent { } func (ts *TimeSpent) minutesToString() string { - s := "" - if ts.Minutes > 0.0 { - intPart, fracPart := math.Modf(ts.Minutes) - if fracPart == 0.0 { - s += fmt.Sprintf("%d minute", uint64(intPart)) - } else { - s += fmt.Sprintf("%.1f minute", ts.Minutes) - } - if ts.Minutes >= 2.0 { - s += "s" - } - } - return s + return formatUnitComponent( + ts.Minutes, + locale.T("UnitMinuteSingular"), + locale.T("UnitMinutePlural"), + ) } func (ts *TimeSpent) hoursToString() string { - s := "" - if ts.Hours > 0.0 { - intPart, fracPart := math.Modf(ts.Hours) - if fracPart == 0.0 { - s += fmt.Sprintf("%d hour", uint64(intPart)) - } else { - s += fmt.Sprintf("%.1f hour", ts.Hours) - } - if ts.Hours >= 2.0 { - s += "s" - } - } - return s + return formatUnitComponent( + ts.Hours, + locale.T("UnitHourSingular"), + locale.T("UnitHourPlural"), + ) } func (ts *TimeSpent) daysToString() string { - s := "" - if ts.Days > 0.0 { - intPart, fracPart := math.Modf(ts.Days) - if fracPart == 0.0 { - s += fmt.Sprintf("%d day", uint64(intPart)) - } else { - s += fmt.Sprintf("%.1f day", ts.Days) - } - if ts.Days >= 2.0 { - s += "s" - } - } - return s + return formatUnitComponent( + ts.Days, + locale.T("UnitDaySingular"), + locale.T("UnitDayPlural"), + ) } func (ts *TimeSpent) weeksToString() string { - s := "" - if ts.Weeks > 0.0 { - intPart, fracPart := math.Modf(ts.Weeks) - if fracPart == 0.0 { - s += fmt.Sprintf("%d week", int64(intPart)) - } else { - s += fmt.Sprintf("%.1f week", ts.Weeks) - } - if ts.Weeks >= 2.0 { - s += "s" - } - } - return s + return formatUnitComponent( + ts.Weeks, + locale.T("UnitWeekSingular"), + locale.T("UnitWeekPlural"), + ) } func (ts *TimeSpent) monthsToString() string { + return formatUnitComponent( + ts.Months, + locale.T("UnitMonthSingular"), + locale.T("UnitMonthPlural"), + ) +} + +func formatUnitComponent(value float64, singularUnit string, pluralUnit string) string { s := "" - if ts.Months > 0.0 { - intPart, fracPart := math.Modf(ts.Months) - if fracPart == 0.0 { - s += fmt.Sprintf("%d month", int64(intPart)) + if value > 0.0 { + var unit string + if value >= 2.0 { + unit = pluralUnit } else { - s += fmt.Sprintf("%.1f month", ts.Months) + unit = singularUnit } - if ts.Months >= 2.0 { - s += "s" + intPart, fracPart := math.Modf(value) + if fracPart == 0.0 { + s += fmt.Sprintf("%d %s", int64(intPart), unit) + } else { + s += fmt.Sprintf("%.1f %s", value, unit) } } return s diff --git a/go.mod b/go.mod index f171786..13780c9 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,40 @@ -module github.com/goutte/gitime +module github.com/goutte/git-spend -go 1.19 +go 1.20 require ( + github.com/BurntSushi/toml v1.0.0 + github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.2 github.com/tsuyoshiwada/go-gitlog v0.0.1 + golang.org/x/text v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) +replace github.com/spf13/cobra => github.com/Goutte/cobra v0.0.0-20230404093907-b279579671a9 + +replace github.com/tsuyoshiwada/go-gitlog => github.com/Goutte/go-gitlog v0.0.0-20240406102306-1efcfa30a305 + require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df // indirect + golang.org/x/sys v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 3db1f4b..4d7f1cc 100644 --- a/go.sum +++ b/go.sum @@ -37,7 +37,13 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 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/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Goutte/cobra v0.0.0-20230404093907-b279579671a9 h1:6DqPA8KTHj+BmPBB5J7viX8jgPxuYcLqoUjxj+IfemQ= +github.com/Goutte/cobra v0.0.0-20230404093907-b279579671a9/go.mod h1:kKQTiXzwhSqS83iXVA/L4jqCpe2jy7X+14DyQ1wmImI= +github.com/Goutte/go-gitlog v0.0.0-20240406102306-1efcfa30a305 h1:IM41iOGTwRv6w/zV5emUaLia4j3a8AKsfIZ/OK/xDo8= +github.com/Goutte/go-gitlog v0.0.0-20240406102306-1efcfa30a305/go.mod h1:nVMD+1UEtGO3Dl5TrjnZUmawWuLs/fp83Vwq77MXO98= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -46,6 +52,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -123,8 +130,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -138,6 +145,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= +github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -147,13 +156,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -174,12 +182,13 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tsuyoshiwada/go-gitlog v0.0.1 h1:ZBIfQG2IUmguQzP8llKgAxUvorcYnI+OhzWXi1vpv38= -github.com/tsuyoshiwada/go-gitlog v0.0.1/go.mod h1:kfjd4M5xSFPqL0Mp2pAk0WM9hP9pJkLQiAndAoQHSRc= +github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df h1:Y2l28Jr3vOEeYtxfVbMtVfOdAwuUqWaP9fvNKiBVeXY= +github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df/go.mod h1:pnyouUty/nBr/zm3GYwTIt+qFTLWbdjeLjZmJdzJOu8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -192,6 +201,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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= @@ -226,6 +236,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -257,6 +268,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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= @@ -276,6 +288,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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= @@ -310,16 +323,21 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -372,6 +390,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -471,6 +490,9 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..282f781 --- /dev/null +++ b/install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env sh + +# Installation script for git-spend +# +# Usage: +# curl https://raw.githubusercontent.com/Goutte/git-spend/main/install.sh | sh +# +# Will ask for password, if you want no interaction pipe the curl into "sudo sh" instead. +# +# I've set this up with sh, but I have no objection against using bash instead at some point. +# zsh is fine too, but perhaps has less support out of the box. +# +# Overview +# -------- +# 1. Detect OS & ARCH +# 2. Download the appropriate binary +# 3. Install that binary +# 4. Install man pages +# 5. Install git hook? +# + +# ----- + +set -e + +DOWNLOAD_URL=https://github.com/Goutte/git-spend/releases/latest/download/git-spend +BINARY_INSTALL_PATH=/usr/local/bin +BINARY_FILENAME=git-spend + +# ----- + +echo "You are about to install git-spend on your system." + +# 2. Download the appropriate binary +echo "Let's download the latest release from github.com…" +curl --location ${DOWNLOAD_URL} > "${BINARY_FILENAME}" + +# 3. Install that binary +echo "Installing ${BINARY_FILENAME} to ${BINARY_INSTALL_PATH}/${BINARY_FILENAME} requires superuser privileges…" +sudo install --preserve-timestamps "${BINARY_FILENAME}" "${BINARY_INSTALL_PATH}/${BINARY_FILENAME}" + +# 4. Install man pages +echo "Installing man pages for ${BINARY_FILENAME}…" +sudo "${BINARY_INSTALL_PATH}/git-spend" man --install + +# --- + +echo "All done !" +echo "" +echo "You can now use:" +echo " git spend sum" +echo "in git projects where you have '/spend ' directives in commits." +echo "" +echo "PLEASE MAKE SURE THIS SOFTWARE IS NOT USED TO OPPRESS" +echo "AS IT WOULD BE AGAINST ITS LICENCE -- #CodersUnion" diff --git a/locale/guesser/env.go b/locale/guesser/env.go new file mode 100644 index 0000000..6f9e8b8 --- /dev/null +++ b/locale/guesser/env.go @@ -0,0 +1,41 @@ +package guesser + +import ( + "golang.org/x/text/language" + "os" +) + +// envVariablesHoldingLocale is sorted by decreasing priority (breaks on first found) +// These environment variables are expected to hold a parsable locale (fr_FR, es, en-US, …) +// ADR: https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html +var envVariablesHoldingLocale = []string{ + "GIT_SPEND_LANGUAGE", + "LANGUAGE", + "LC_ALL", + "LANG", +} + +func DetectLanguages(defaultLanguage language.Tag) []string { + var detectedLangs []string + for _, envKey := range envVariablesHoldingLocale { + lang := os.Getenv(envKey) + if lang != "" { + detectedLang := language.Make(lang) + appendLang(&detectedLangs, detectedLang) + } + } + appendLang(&detectedLangs, defaultLanguage) + + return detectedLangs +} + +func appendLang(langs *[]string, lang language.Tag) { + langString := lang.String() + *langs = append(*langs, langString) + + langBase, confidentInBase := lang.Base() + if confidentInBase != language.No { + *langs = append(*langs, langBase.String()) + *langs = append(*langs, langBase.ISO3()) + } +} diff --git a/locale/strings.en.toml b/locale/strings.en.toml new file mode 100644 index 0000000..1aa9ff6 --- /dev/null +++ b/locale/strings.en.toml @@ -0,0 +1,132 @@ +And="and" +Or="or" + + +UnitMonthSingular="month" +UnitMonthPlural="months" +UnitWeekSingular="week" +UnitWeekPlural="weeks" +UnitDaySingular="day" +UnitDayPlural="days" +UnitHourSingular="hour" +UnitHourPlural="hours" +UnitMinuteSingular="minute" +UnitMinutePlural="minutes" + + +CommandRootSummary = "time-tracker using git commits" +CommandRootDescription = """ +Manage time-tracking /spent directives in commit messages. + +Exemples of supported /spend directives: + + /spend 1h30 + /spend 1 month 3 days 7 hours + /spent 15m + +Get help on a subcommand using the --help flag: + + git spend sum --help + +Or read the (equivalent) manpages of the subcommands: + + man git-spend-sum + +Source: https://github.com/Goutte/git-spend +""" + + +CommandSumSummary = "Sum /spent time recorded in commit messages" +CommandSumDescription = """ +The /spend and /spent directives will be parsed and summed +from the commit messages in the current directory's git repository. + +The default target is the current working directory, '.', +but you may specify another target using the --target flag: + + git-spend sum --target + +You can also get a raw number in a specific unit: + + git spend sum --minutes + +You can also restrict to some commit authors, by name or email: + + git spend sum --author=Alice --author=bob@pop.net --author=Eve + +You can restrict to a range of commits, using a commit hash, a tag, +or even HEAD~N. + + git spend sum --since --until + +For example, to get the time spent on the last 15 commits : + + git spend sum --since HEAD~15 + +Or the time spent on a tag since the previous tag : + + git spend sum --since 0.1.0 --until 0.1.1 + +You can also use dates and datetimes, but remember to quote them: + + git spend sum --since 2023-03-21 + git spend sum --since "2023-03-21 13:37:00" + +Other formats are allowed (RFC3339, RFC822, RFC850), +and if you need to set a timezone use the TZ environment variable: + + TZ="Europe/Paris" git spend sum --until "2023-03-31 10:00:00" + +""" +CommandSumFailureStdinAuthors=""" +Flag --author is not supported with --stdin parsing. +Meanwhile, you can use --author on git log, like so: + + git log --author Bob | git spend sum --stdin +""" +CommandSumFailureStdinNoMerges=""" +Flag --no-merges is not supported with --stdin parsing. +Meanwhile, you can use --no-merges on git log, like so: + + git log --no-merges | git spend sum --stdin +""" +CommandSumFailureStdinSince=""" +Flag --since is not supported with --stdin parsing. +Meanwhile, you can use --since on git log, like so: + + git log --since tags/0.1.0 | git spend sum --stdin +""" +CommandSumFailureStdinUntil=""" +Flag --until is not supported with --stdin parsing. +Meanwhile, you can use --until on git log, like so: + + git log --until 2023-03-31 | git spend sum --stdin +""" +CommandSumFailureStdinTarget=""" +Flag --target is not supported with --stdin parsing. +What would it mean, to you ? Contribs are welcome. +""" +CommandSumFailureNothingFound="No time-tracking /spend directives found in commits" +CommandSumFailureNothingFoundForAuthors="by authors %s" +CommandSumFailureNothingFoundAfterSince="after %s" +CommandSumFailureNothingFoundBeforeUntil="before %s" + +CommandSumFlagMinutesHelp="show sum in minutes" +CommandSumFlagHoursHelp="show sum in hours (1 hour = %.1f minutes)" +CommandSumFlagDaysHelp="show sum in days (1 day = %.1f hours)" +CommandSumFlagWeeksHelp="show sum in weeks (1 week = %.1f days)" +CommandSumFlagMonthsHelp="show sum in months (1 month = %.1f weeks)" + +CommandSumFlagTargetHelp="target this directory instead of the working directory" +CommandSumFlagStdinHelp="read stdin instead of target's git log" +CommandSumFlagAuthorsHelp="only use commits by these authors (can be repeated)" +CommandSumFlagNoMergesHelp="ignore merge commits" +CommandSumFlagSinceHelp="only use commits after this ref (exclusive)" +CommandSumFlagUntilHelp="only use commits before this ref (inclusive)" + +CommandManSummary="create man pages for git-spend" +CommandManDescription=""" +Generate man pages in the user's locale. (defaults to english) +""" +CommandManFlagOutput="where to create the man pages" +CommandManFlagInstall="create man pages in %s (overrides --output)" \ No newline at end of file diff --git a/locale/strings.fr.toml b/locale/strings.fr.toml new file mode 100644 index 0000000..f86fdb5 --- /dev/null +++ b/locale/strings.fr.toml @@ -0,0 +1,137 @@ +And="et" +Or="ou" + + +UnitMonthSingular="mois" +UnitMonthPlural="mois" +UnitWeekSingular="semaine" +UnitWeekPlural="semaines" +UnitDaySingular="jour" +UnitDayPlural="jours" +UnitHourSingular="heure" +UnitHourPlural="heures" +UnitMinuteSingular="minute" +UnitMinutePlural="minutes" + + +CommandRootSummary = "mesurer le temps passé à coder" +CommandRootDescription = """ +Gérer les directives /spend inscrites dans les messages de commit. + +Exemples de directives /spend supportées: + + /spend 1h30 + /spend 1 month 3 days 7 hours + /spent 15m + +Pour obtenir de l'aide sur une subcommande: + + git spend sum --help + +Ou lire le manuel (équivalent) d'une subcommande: + + man git-spend-sum + +Source: https://github.com/Goutte/git-spend +""" + + +CommandSumSummary = "Cumule le temps enregistré dans les commits via /spend" +CommandSumDescription = """ +Les directives /spend et /spent des messages de commit +du dépôt git du répertoire courant seront lues et +leurs durées additionnées. + +Le répertoire cible par défaut est le répertoire courant, `.`, +mais vous pouvez en viser un autre avec --target : + + git-spend sum --target + +Vous pouvez obtenir un résultat numérique en précisant une unité : + + git spend sum --minutes + +Vous pouvez également filtrer par auteurs, avec leurs noms ou courriels: + + git spend sum --author=Alice --author=bob@pop.net --author=Eve + +Vous pouvez limiter à une plage de commits, +en utilisant un hash de commit, une balise ou même HEAD~N. + + git spend sum --since --until + +Par exemple, pour obtenir le temps passé sur les 15 derniers commits : + + git spend sum --since HEAD~15 + +Ou le temps passé sur un tag depuis le tag précédent : + + git spend sum --since 0.1.0 --until 0.1.1 + +Vous pouvez utiliser des dates, mais n'oubliez pas les guillemets : + + git spend sum --since "2023-03-21 13:37:00" + git spend sum --since 2023-03-21 + +D'autres formats sont acceptés (RFC3339, RFC822, RFC850), et si vous +avez besoin de spécifier la zone horaire, utilisez TZ : + + TZ="Europe/Paris" git spend sum --until "2023-03-31 10:00:00" + +""" +CommandSumFailureStdinAuthors=""" +Le paramètre --author n'est pas utilisable avec --stdin. +Vous pouvez cependant utiliser --author sur git log, comme ceci : + + git log --author Bob | git spend sum --stdin + +""" +CommandSumFailureStdinNoMerges=""" +Le paramètre --no-merges n'est pas utilisable avec --stdin. +Vous pouvez cependant utiliser --no-merges sur git log, comme ceci : + + git log --no-merges | git spend sum --stdin + +""" +CommandSumFailureStdinSince=""" +Le paramètre --since n'est pas utilisable avec --stdin. +Vous pouvez cependant utiliser --since sur git log, comme ceci : + + git log --since tags/0.1.0 | git spend sum --stdin + +""" +CommandSumFailureStdinUntil=""" +Le paramètre --until n'est pas utilisable avec --stdin. +Vous pouvez cependant utiliser --until sur git log, comme ceci : + + git log --until 2023-03-31 | git spend sum --stdin + +""" +CommandSumFailureStdinTarget=""" +Le paramètre --target est exclusif avec --stdin. +""" +CommandSumFailureNothingFound="Aucune directive de chronometrage /spend trouvée dans les commits" +CommandSumFailureNothingFoundForAuthors="de %s" +CommandSumFailureNothingFoundAfterSince="après %s" +CommandSumFailureNothingFoundBeforeUntil="avant %s" + +CommandSumFlagMinutesHelp="afficher la somme en minutes" +CommandSumFlagHoursHelp="afficher la somme en heures (1 heure = %.1f minutes)" +CommandSumFlagDaysHelp="afficher la somme en jours (1 jour = %.1f heures)" +CommandSumFlagWeeksHelp="afficher la somme en semaines (1 semaine = %.1f jours)" +CommandSumFlagMonthsHelp="afficher la somme en mois (1 mois = %.1f semaines)" + +CommandSumFlagTargetHelp="cibler ce dossier au lieu du dossier courant" +CommandSumFlagStdinHelp="lire depuis l'entrée standard plutôt que git log" +CommandSumFlagAuthorsHelp="filtrer par nom ou courriel (peut être répété)" +CommandSumFlagNoMergesHelp="ignorer les commits de merge" +CommandSumFlagSinceHelp="n'utiliser que les commits après cette ref (exclusive)" +CommandSumFlagUntilHelp="n'utiliser que les commits avant cette ref (inclusive)" + +CommandManSummary="créer le manuel de git-spend" +CommandManDescription=""" +Génère le manuel de git-send dans la langue de l'utilisateur. +(anglais par défaut) +""" +CommandManFlagOutput="où créer les fichiers du manuel" +CommandManFlagInstall="créer le manuel dans %s (remplace --output)" \ No newline at end of file diff --git a/locale/translator.go b/locale/translator.go new file mode 100644 index 0000000..b6ff4f7 --- /dev/null +++ b/locale/translator.go @@ -0,0 +1,59 @@ +package locale + +import ( + "embed" + "fmt" + "github.com/BurntSushi/toml" + "github.com/goutte/git-spend/locale/guesser" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +// defaultLanguage should be language.Esperanto 💡 ("eo") +var defaultLanguage = language.English +var domain = "strings" +var extension = "toml" + +// localeFS points to an embedded filesystem of TOML translation files (eases binary distribution) +// +//go:embed *.toml +var localeFS embed.FS + +// Localizer can be used to fetch localized messages +var Localizer *i18n.Localizer + +// T fetches the translation of the specified key +func T(key string) string { + localized, _ := Localizer.Localize(&i18n.LocalizeConfig{ + MessageID: key, + }) + + return localized +} + +// Tf fetches the translation of the specified key and formats it like Sprintf +func Tf(key string, args ...any) string { + localized, _ := Localizer.Localize(&i18n.LocalizeConfig{ + MessageID: key, + }) + + return fmt.Sprintf(localized, args...) +} + +func loadTranslationFiles(bundle *i18n.Bundle, languages []string) { + for _, lang := range languages { + _, _ = bundle.LoadMessageFileFS( + localeFS, + fmt.Sprintf("%s.%s.%s", domain, lang, extension), + ) + } +} + +func init() { + bundle := i18n.NewBundle(defaultLanguage) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + + detectedLanguages := guesser.DetectLanguages(defaultLanguage) + loadTranslationFiles(bundle, detectedLanguages) + Localizer = i18n.NewLocalizer(bundle, detectedLanguages...) +} diff --git a/main.go b/main.go index 575f3cc..8a53f6a 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,15 @@ package main import ( - "github.com/goutte/gitime/cmd" + "github.com/goutte/git-spend/cmd" "log" ) func main() { + log.SetFlags(0) + err := cmd.Execute() if err != nil { - log.Fatalln("Failure:", err) + log.Fatalln("failure:", err) } } diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..54be9c8 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,42 @@ +In this directory are the manifest configurations for various packaging platforms. + + +## Go get + + go get -u github.com/goutte/git-spend + + +## Github Releases + +Things I built using `make release`: + +- Linux: https://github.com/Goutte/git-spend/releases/latest/download/git-spend +- Windows: https://github.com/Goutte/git-spend/releases/latest/download/git-spend.exe + +> Wanted: CI releases with artifacts + + +## Flatpak + +Packaging with flatpak is not yet supported. + +The manifest is half-there, but we need to solve these roadblocks first. + +### Roadblocks + +- Git Access (how?) +- Filesystem Access: + - global (not recommended) + - https://docs.flatpak.org/en/latest/sandbox-permissions.html#portals + - overrides (meh, but quick) + + +## Debian + +The final binary weighs almost `2 Mio`. +I believe that with enough patience, skill and work, one could make +a shell (or bash) version of `git-spend` for a fraction of that. + +I'd reserve the debian package for such a rewrite. + + diff --git a/packaging/com.github.goutte.Gitspend.yml b/packaging/com.github.goutte.Gitspend.yml new file mode 100644 index 0000000..1061e60 --- /dev/null +++ b/packaging/com.github.goutte.Gitspend.yml @@ -0,0 +1,32 @@ +# Flatpak manifest +# https://docs.flatpak.org/en/latest/first-build.html + +# Setup +# ----- +# apt install flatpak flatpak-builder +# flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +# flatpak install flathub org.freedesktop.Platform//22.08 org.freedesktop.Sdk//22.08 +# echo export 'XDG_DATA_DIRS="$HOME/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:$XDG_DATA_DIRS"' >> ~/.xsessionrc + +# Build +# ----- +# make release +# flatpak-builder --user --install --force-clean build/flatpak packaging/com.github.goutte.Gitspend.yml +# flatpak run io.github.goutte.Gitspend sum +# > cannot read git log: "git" does not exists +# … +# So we need Git and perhaps also filesystem access (either via file chooser → preferred ; or global), before publishing. + +app-id: io.github.goutte.Gitspend +runtime: org.freedesktop.Platform +runtime-version: '22.08' +sdk: org.freedesktop.Sdk +command: git-spend +modules: + - name: git-spend + buildsystem: simple + build-commands: + - install -D git-spend /app/bin/git-spend + sources: + - type: file + path: ../build/git-spend diff --git a/packaging/debian/.gitignore b/packaging/debian/.gitignore new file mode 100644 index 0000000..96b1b97 --- /dev/null +++ b/packaging/debian/.gitignore @@ -0,0 +1 @@ +git-spend diff --git a/packaging/debian/git-spend_1.2.0.orig.tar.gz b/packaging/debian/git-spend_1.2.0.orig.tar.gz new file mode 100644 index 0000000..c641734 Binary files /dev/null and b/packaging/debian/git-spend_1.2.0.orig.tar.gz differ diff --git a/packaging/debian/itp-git-spend.txt b/packaging/debian/itp-git-spend.txt new file mode 100644 index 0000000..dabe1ff --- /dev/null +++ b/packaging/debian/itp-git-spend.txt @@ -0,0 +1,245 @@ +From: "Antoine Goutenoir" +To: submit@bugs.debian.org +Subject: ITP: git-spend -- Sum the time-tracking /spend commands of git commit messages. +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit +X-Debbugs-CC: debian-devel@lists.debian.org, debian-go@lists.debian.org + +Package: wnpp +Severity: wishlist +Owner: "Antoine Goutenoir" + +* Package name : git-spend + Version : 1.2.0-1 + Upstream Author : Antoine Goutenoir +* URL : https://github.com/Goutte/git-spend +* License : MIT + Programming Lang: Go + Description : Sum the time-tracking "/spend" commands of git commit messages. + + git-spend : time tracker using git commit message commands + . + Purpose + . + Collect, addition and return all the /spend and /spent time-tracking + directives in git commit messages. + . + | This looks at the git log of the currently checked out branch of the + | working directory, + | and therefore requires git to be installed on your system. + . + By Example + . + Say you are in the directory of a project with one commit like so : + . + feat(crunch): implement a nice feature + . + Careful, it's still sharp. + /spend 10h30 + . + Running: + . + $ git spend sum + . + would yield: + . + | 1 day 2 hours 30 minutes + . + Of course, *git-spend* really shines when you have multiple commits with + /spend commands that you want to tally and sum. + . + | 💡 You can use git-spend sum or git spend sum, they are equivalent. + . + Specifications + . + We assume 8 hours per day, 5 days per week, 4 weeks per month. *(like + Gitlab does)* These can be configured at runtime if needed, using + environment variables. + . + The **complete specification** can be found in the rules + (/gitime/gitime_test_data.yaml) of the test data, and in excruciating + detail in the grammar (/gitime/grammar.go). + . + The acceptance testing suite (/test/features.bats) also holds many usage + examples. + . + Usage + . + Go into your git-versioned project's directory: + . + cd + . + and run: + . + git spend sum + . + | 2 days 1 hour 42 minutes + . + Or run git-spend from anywhere, but specify the --target directory (which + defaults to .): + . + git spend sum --target + . + | 2 days 1 hour 42 minutes + . + | ⛑ Use git spend sum --help or man git-spend-sum to see all the options. + | Meanwhile, let's look at some available options, below. + . + Format the output + . + You can get the spent time in a specific unit : + . + git spend sum --minutes + git spend sum --hours + git spend sum --days + . + | These values will always be rounded to integers, for convenience, + | although *git-spend* does understand floating point numbers in /spend + | directives. + . + Filter by commit authors + . + You can track the time of specified authors only, by name or email : + . + git spend sum --author Alice --author bob@email.net + . + Exclude merge commits + . + You can also exclude merge commits : + . + git spend sum --no-merges + . + Restrict to a range of commits + . + You can restrict to a range of commits, using a commit hash, a tag, or + even HEAD~N. + . + git spend sum --since --until + . + For example, to get the time spent on the last 15 commits : + . + git spend sum --since HEAD~15 + . + Or the time spent on a tag since previous tag : + . + git spend sum --since 0.1.0 --until 0.1.1 + . + You can also use *dates* and *datetimes*, but remember to quote them if + you specify the time: + . + git spend sum --since 2023-03-21 + git spend sum --since "2023-03-21 13:37:00" + . + | 📅 Other supported time formats: RFC3339 (https://www.rfc- + | editor.org/rfc/rfc3339), RFC822 + (https://www.w3.org/Protocols/rfc822/), + | RFC850 (https://www.rfc-editor.org/rfc/rfc850). + | If you need a specific timezone, try setting the TZ environment + | variable: + | TZ="Europe/Paris" git-spend sum --since 2023-03-21 + . + Download + . + Direct download + . + You can ⮋ download the binary (https://github.com/Goutte/git- + spend/releases/latest/download/git-spend) straight from the latest build + in the releases (https://github.com/Goutte/git-spend/releases), and move + it anywhere in your $PATH, such as /usr/local/bin/git-spend for example. + . + | ⚠ Remember to enable the execution bit with chmod u+x ./git-spend, for + | example. + . + There is an *experimental* install script that does exactly this, plus + man pages generation: + . + curl https://raw.githubusercontent.com/Goutte/git-spend/main/install.sh + | sh + . + | 🐧 This script only works for linux/amd64, for now. *Stigmergy?* + . + Via go get + . + You can also install via go get (hopefully) : + . + go get -u github.com/goutte/git-spend + . + or go install: + . + go install github.com/goutte/git-spend + . + | If that fails, you can install by cloning and running make install. + . + Advanced Usage + . + Read from standard input + . + You can also directly parse messages from stdin instead of attempting to + read the git log: + . + git log > git.log + cat git.log | git-spend sum --stdin + . + | git spend ignores standard input otherwise. + . + Configure the time modulo + . + If you live somewhere where work hours per week are limited (to 35 for + example) in order to mitigate labor oppression tactics from monopoly + hoarders, you can use environment variables to control how time is + "rolled over" between units : + . + GIT_SPEND_HOURS_IN_ONE_DAY=7 git-spend sum + . + Here are the available environment variables : + . + * GIT_SPEND_MINUTES_IN_ONE_HOUR (default: 60) + * GIT_SPEND_HOURS_IN_ONE_DAY (default: 8) + * GIT_SPEND_DAYS_IN_ONE_WEEK (default: 5) + * GIT_SPEND_WEEKS_IN_ONE_MONTH (default: 4) + . + Install the man pages + . + If you installed via direct download, you might want to install the man + pages: + . + sudo git spend man --install + . + | git help spend will then work as expected. + . + Develop + . + git clone https://github.com/Goutte/git-spend.git + cd git-spend + go get + go run main.go + . + Build & Run & Install + . + The binaries in the releases are built by our Continuous Integration + (/.github/workflows/release.yml). + . + Nevertheless, if you want to build your own git-spend, you can clone this + project and run: + . + make + make install + . + | upx (https://upx.github.io/) is used to reduce the binary size in + make + | install-release. + . + Build for other platforms + . + You may use the GOOS and GOARCH environment variables to control the + build targets: + . + GOOS= GOARCH= go build -o build/git-spend + . + . + To list available targets (os/arch), you can run: + . + go tool dist list + . + | There's an example in the Makefile (/Makefile), with the recipe make + | build-windows-amd64. diff --git a/test/features.bats b/test/features.bats index 83929b4..c7ee46b 100644 --- a/test/features.bats +++ b/test/features.bats @@ -1,288 +1,499 @@ #!/usr/bin/env bats +# Acceptance test suite, made with BATS. # https://github.com/bats-core/bats-core # Run: # make test-acceptance -# We use gitime's own repo as fixture. +# We use git-spend's own repo as fixture for tests. (🐕 woof) # We copy this project into a temporary fixture dir (in RAM), # and then have it check out the appropriate fixture-XX tag, # and finally run integration testing on that temporary repo. -TMP_FIXTURE_DIR="/tmp/gitime-test" +# See the setup() BATS hook defined at the bottom of this file. +# THIS DIRECTORY WILL BE `RM -RF` SO BEWARE OF WHAT'S IN THERE. +TMP_FIXTURE_DIR="/tmp/git-spend-fixture" -setup() { - load 'test_helper/bats-support/load' - load 'test_helper/bats-assert/load' - - TESTS_DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" - PROJECT_DIR="$( dirname "$TESTS_DIR" )" - COVERAGE_DIR=${PROJECT_DIR}/test-coverage - gitime=${PROJECT_DIR}/build/gitime - - if [ "$GITIME_COVERAGE" == "1" ] ; then - echo "Setting up coverage in ${COVERAGE_DIR}" - mkdir -p "${COVERAGE_DIR}" - export GOCOVERDIR=${COVERAGE_DIR} - gitime="${gitime}-coverage" - fi - - export GITIME_NO_STDIN=1 - - cp -R "$PROJECT_DIR" "$TMP_FIXTURE_DIR" - cd "$TMP_FIXTURE_DIR" || exit - - git stash - git checkout tags/fixture-00 -b fixture-00 - echo "success: ignore the unable to rmdir warning above (benign)" - - git log fixture-00 > fixture-00.log - git log 0.1.0 > 0.1.0.log +@test "git-spend" { + run "${git_spend}" + assert_success + assert_output --partial 'Manage time-tracking /spent directives in commit messages' } -teardown() { - rm -rf $TMP_FIXTURE_DIR - rm -f fixture-00.log - rm -f 0.1.0.log +@test "git-spend hohohoooo should fail" { + run "${git_spend}" hohohoooo + assert_failure } -@test "gitime" { - run $gitime +@test "git-spend help sum" { + run "${git_spend}" help sum assert_success - assert_output --partial 'Gather information about /spent time from commit messages' } -@test "gitime hohohoooo should fail" { - run $gitime hohohoooo - assert_failure -} - -@test "gitime help sum" { - run $gitime help sum +@test "git-spend sum --help" { + run "${git_spend}" sum --help assert_success + assert_output --partial 'The /spend and /spent directives will be parsed and summed' } -@test "gitime sum --help" { - run $gitime sum --help +@test "git-spend sum" { + run "${git_spend}" sum assert_success + assert_output "1 week 3 hours" } -@test "gitime sum" { - run $gitime sum +@test "git-spend sum --target " { + cd "${PROJECT_DIR}" + run "${git_spend}" sum --target "${TMP_FIXTURE_DIR}" assert_success assert_output "1 week 3 hours" } -@test "gitime sum --minutes" { - run $gitime sum --minutes +@test "git-spend sum --target <404 dir> should fail" { + run "${git_spend}" sum --target "/to/code/or/not/to/code" + assert_failure +} + +@test "git-spend sum --minutes" { + run "${git_spend}" sum --minutes assert_success assert_output "2580" } -@test "gitime sum --hours" { - run $gitime sum --hours +@test "git-spend sum --hours" { + run "${git_spend}" sum --hours assert_success assert_output "43" } -@test "gitime sum --days" { - run $gitime sum --days +@test "git-spend sum --days" { + run "${git_spend}" sum --days assert_success assert_output "5" } -@test "gitime sum --weeks" { - run $gitime sum --weeks +@test "git-spend sum --weeks" { + run "${git_spend}" sum --weeks assert_success assert_output "1" } -@test "gitime sum --months" { - run $gitime sum --months +@test "git-spend sum --months" { + run "${git_spend}" sum --months assert_success assert_output "0" } -@test "gitime sum --author Goutte" { - run $gitime sum --author Goutte +@test "git-spend sum unit formats are mutually exclusive" { + run "${git_spend}" sum --months --days + assert_failure + run "${git_spend}" sum --hours --minutes --weeks + assert_failure +} + +@test "git-spend sum --author Goutte" { + run "${git_spend}" sum --author Goutte assert_success assert_output "1 week 3 hours" } -@test "gitime sum --author antoine@goutenoir.com" { - run $gitime sum --author antoine@goutenoir.com +@test "git-spend sum --author antoine@goutenoir.com" { + run "${git_spend}" sum --author antoine@goutenoir.com assert_success assert_output "1 week 3 hours" } -@test "gitime sum --author notfound (should fail)" { - run $gitime sum --author notfound +@test "git-spend sum --author notfound (should fail)" { + run "${git_spend}" sum --author notfound # shouldn't we fail, here? TBD #assert_failure assert_success # …meanwhile - assert_output "No time-tracking directives /spend or /spent found in commits." + assert_output --partial "No time-tracking /spend directives found in commits" } -@test "gitime sum --since " { - run $gitime sum --since 786a30642fe37368b0b65cbca8ca1a5c4b6c97b8 +@test "git-spend sum --since " { + run "${git_spend}" sum --since 786a30642fe37368b0b65cbca8ca1a5c4b6c97b8 assert_success assert_output "1 day 6 hours 3 minutes" } -@test "gitime sum --since " { - run $gitime sum --since 786a3064 +@test "git-spend sum --since " { + run "${git_spend}" sum --since 786a3064 assert_success assert_output "1 day 6 hours 3 minutes" } -@test "gitime sum --since " { - run $gitime sum --since 0.2.0 +@test "git-spend sum --since " { + run "${git_spend}" sum --since 0.2.0 assert_success assert_output "1 day 6 hours 3 minutes" } -@test "gitime sum --since " { - run $gitime sum --since caca999 +@test "git-spend sum --since " { + run "${git_spend}" sum --since caca999 assert_failure } -@test "gitime sum --since (should fail)" { - run $gitime sum --since lololololo +@test "git-spend sum --since (should fail)" { + run "${git_spend}" sum --since lololololo assert_failure } -@test "gitime sum --until " { - run $gitime sum --until 786a30642fe37368b0b65cbca8ca1a5c4b6c97b8 +@test "git-spend sum --until " { + run "${git_spend}" sum --until 786a30642fe37368b0b65cbca8ca1a5c4b6c97b8 assert_success assert_output "3 days 4 hours 57 minutes" } -@test "gitime sum --until " { - run $gitime sum --until 786a3064 +@test "git-spend sum --until " { + run "${git_spend}" sum --until 786a3064 assert_success assert_output "3 days 4 hours 57 minutes" } -@test "gitime sum --until " { - run $gitime sum --until 0.2.0 +@test "git-spend sum --until " { + run "${git_spend}" sum --until 0.2.0 assert_success assert_output "3 days 4 hours 57 minutes" } -@test "gitime sum --until " { - run $gitime sum --until caca666 +@test "git-spend sum --until " { + run "${git_spend}" sum --until caca666 assert_failure } -@test "gitime sum --until " { - run $gitime sum --until trololololo +@test "git-spend sum --until " { + run "${git_spend}" sum --until trololololo assert_failure } -@test "gitime sum --until 0.1.0" { - run $gitime sum --until 0.1.0 +@test "git-spend sum --until 0.1.0" { + run "${git_spend}" sum --until 0.1.0 assert_success assert_output "1 day 7 hours 57 minutes" } -@test "gitime sum --until tags/" { - run $gitime sum --until 0.1.0 +@test "git-spend sum --until tags/" { + run "${git_spend}" sum --until tags/0.1.0 assert_success assert_output "1 day 7 hours 57 minutes" } -@test "gitime sum --since 0.1.0" { - run $gitime sum --since 0.1.0 +@test "git-spend sum --since 0.1.0" { + run "${git_spend}" sum --since 0.1.0 assert_success assert_output "3 days 3 hours 3 minutes" } -@test "gitime sum --since 0.1.0 --until 0.1.1" { - run $gitime sum --since 0.1.0 --until 0.1.1 +@test "git-spend sum --since 0.1.0 --until 0.1.1" { + run "${git_spend}" sum --since 0.1.0 --until 0.1.1 assert_success assert_output "30 minutes" } -@test "gitime sum --since " { - # Sun Mar 26 22:11:03 2023 of 4527140510c2b77a9f2a6eb947b5391d4e2173a9 - run $gitime sum --since 2023-03-27 +@test "git-spend sum --since " { + run "${git_spend}" sum --since 2023-03-27 assert_success assert_output "2 hours" } -@test "gitime sum --since " { - run $gitime sum --since "2023-03-26 22:15:00" +@test "git-spend sum --since " { + run "${git_spend}" sum --since "2023-03-26 22:15:00" assert_success assert_output "2 hours 1 minute" - # We'd want, but no cigar ; time parsing in Golang is quite weird - #run $gitime sum --since "2023-03-26 22:15" + # Want to tolerate missing minutes, but no cigar ; time parsing in Golang is quite peculiar + # This can still be done, and would be nice, but I find my solution to be … inelegant. #mr-welcome + #run "${git_spend}" sum --since "2023-03-26 22:15" + #run "${git_spend}" sum --since "2023-03" # and perhaps this as well? #assert_success } -@test "gitime sum --since " { - run $gitime sum --since 2023-03-26T22:15:00Z +@test "git-spend sum --since " { + run "${git_spend}" sum --since 2023-03-26T22:15:00Z assert_success assert_output "2 hours 1 minute" } -@test "gitime sum --until " { - run $gitime sum --until 2023-03-25 +@test "git-spend sum --until " { + run "${git_spend}" sum --until 2023-03-25 assert_success assert_output "1 day 3 hours 55 minutes" } -@test "gitime sum --since --until " { - run $gitime sum --since "2023-03-25 03:30:00" --until "2023-03-25 13:37:00" +@test "git-spend sum --since --until " { + run "${git_spend}" sum --since "2023-03-25 03:30:00" --until "2023-03-25 13:37:00" assert_success assert_output "2 hours 15 minutes" } -@test "gitime sum does not accept mixed dates and refs in ranges" { - run $gitime sum --until 2023-03-27 --since 0.1.0 +@test "git-spend sum but nothing was found" { + run "${git_spend}" sum --since "2023-03-25" --until "2023-03-25" + assert_success + assert_output --partial "No time-tracking /spend directives found in commits" +} + +@test "git-spend sum does not accept mixed dates and refs in ranges" { + run "${git_spend}" sum --until 2023-03-27 --since 0.1.0 assert_failure - run $gitime sum --since 2023-03-24 --until 0.2.0 + run "${git_spend}" sum --since 2023-03-24 --until 0.2.0 assert_failure } -@test "gitime sum using stdin" { - export GITIME_NO_STDIN=0 - run bash -c "cat fixture-00.log | $gitime sum" -# run $gitime sum < fixture-00.log +@test "git-spend sum ignores stdin by default" { + run bash -c "cat 0.1.0.log | $git_spend sum" + assert_success + assert_output "1 week 3 hours" # and not "1 day 7 hours 57 minutes" for 0.1.0 +} + +@test "git-spend sum --stdin using |" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin" assert_success assert_output "1 week 3 hours" + + run bash -c "cat 0.1.0.log | $git_spend sum --stdin" + assert_success + assert_output "1 day 7 hours 57 minutes" # and not "1 week 3 hours" } -@test "gitime sum using another stdin" { - export GITIME_NO_STDIN=0 - run bash -c "cat 0.1.0.log | $gitime sum" -# run bash -c "$gitime sum < 0.1.0.log" +@test "git-spend sum --stdin using <" { + run bash -c "$git_spend sum --stdin < fixture-00.log" + assert_success + assert_output "1 week 3 hours" + + run bash -c "$git_spend sum --stdin < 0.1.0.log" assert_success assert_output "1 day 7 hours 57 minutes" } -@test "gitime sum using stdin does not accept --no-merges" { - export GITIME_NO_STDIN=0 - run bash -c "cat fixture-00.log | $gitime sum --no-merges" -# run bash -c "$gitime sum --no-merges < fixture-00.log" +@test "git-spend sum --stdin does not accept --target" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin --target ${PROJECT_DIR}" assert_failure } -@test "gitime sum using stdin does not accept --author" { - export GITIME_NO_STDIN=0 - run bash -c "cat fixture-00.log | $gitime sum --author Goutte" -# run bash -c "$gitime sum --author Goutte < fixture-00.log" +@test "git-spend sum --stdin does not accept --no-merges" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin --no-merges" assert_failure } -@test "gitime sum using stdin does not accept --since" { - export GITIME_NO_STDIN=0 - run bash -c "cat fixture-00.log | $gitime sum --since 0.1.0" -# run bash -c "$gitime sum --since 0.1.0 < fixture-00.log" +@test "git-spend sum --stdin does not accept --author" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin --author Goutte" assert_failure } -@test "gitime sum using stdin does not accept --until" { - export GITIME_NO_STDIN=0 - run bash -c "cat fixture-00.log | $gitime sum --until 0.1.0" -# r0un bash -c "$gitime sum --until 0.1.0 < fixture-00.log" +@test "git-spend sum --stdin does not accept --since" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin --since 0.1.0" assert_failure -} \ No newline at end of file +} + +@test "git-spend sum --stdin does not accept --until" { + run bash -c "cat fixture-00.log | $git_spend sum --stdin --until 0.1.0" + assert_failure +} + +@test "Support for GIT_SPEND_MINUTES_IN_ONE_HOUR" { + export GIT_SPEND_MINUTES_IN_ONE_HOUR=10 + run "${git_spend}" sum --since HEAD~1 --minutes + assert_success + assert_output "20" # instead of 120 (converted from 2h) +} + +@test "Support for alias GIT_SPEND_MINUTES_PER_HOUR" { + export GIT_SPEND_MINUTES_PER_HOUR=30 + run "${git_spend}" sum --since HEAD~1 --minutes + assert_success + assert_output "60" # instead of 120 (converted from 2h) +} + +@test "LANGUAGE=fr_FR git-spend" { + # shellcheck disable=SC2030 + export LANGUAGE=fr_FR + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "LANGUAGE=fr git-spend" { + # shellcheck disable=SC2030 + export LANGUAGE=fr + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "LC_ALL=fr_FR git-spend" { + unset LANGUAGE + # shellcheck disable=SC2030 + export LC_ALL=fr_FR + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "LANG=fr_FR git-spend" { + unset LANGUAGE + unset LC_ALL + # shellcheck disable=SC2030 + export LANG=fr_FR + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "LANG=fr git-spend sum" { + unset LANGUAGE + unset LC_ALL + # shellcheck disable=SC2030 + export LANG=fr + run "${git_spend}" sum --until tags/0.1.0 + assert_success + assert_output '1 jour 7 heures 57 minutes' +} + +@test "LC_ALL has priority over LANG" { + unset LANGUAGE + export LANG=en_US + # shellcheck disable=SC2030 + export LC_ALL=fr_FR + + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "LANGUAGE has priority over LC_ALL" { + # shellcheck disable=SC2030 + export LANGUAGE=fr_FR + export LC_ALL=en_US + + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "Default language is english" { + unset LANGUAGE + unset LC_ALL + unset LANG + + run "${git_spend}" + assert_success + assert_output --partial 'Manage time-tracking /spent directives in commit messages' +} + +@test "Unhandled language falls back to english" { + # shellcheck disable=SC2030 + export LANGUAGE="es_CL" + run "${git_spend}" + assert_success + assert_output --partial 'Manage time-tracking /spent directives in commit messages' +} + +@test "Unhandled locale should fallback to default locale if language is handled" { + export LANGUAGE="fr_CA" + run "${git_spend}" + assert_success + assert_output --partial 'Gérer les directives /spend inscrites dans les messages de commit' +} + +@test "Generate man pages" { + run "${git_spend}" man + assert_success +} + +@test "Generating man pages" { + run "${git_spend}" man --output /usr/local/share/man/man8 + run "${git_spend}" man --install + # no assertions for now (CI runs root-like) +} + +@test "Installing man pages requires sudo" { + skip # hehe, CI is root, I forgot ; coverage will lower, but that's OK + + run "${git_spend}" man --output /usr/local/share/man/man8 + assert_failure + assert_output --partial 'permission denied' + + run "${git_spend}" man --output /usr/share/man/man8 + assert_failure + assert_output --partial 'permission denied' + + run "${git_spend}" man --install + assert_failure + assert_output --partial 'permission denied' +} + +@test "Rewrite /spend HEAD" { + skip # wip + + # 1. Install the hooks + + run "${git_spend}" hook --install + + # 2.a. Add a commit, but pretend it was 5 minutes ago + + touch some_new_file && git add some_new_file + minutes_ago=$(date -d "-5minutes" --iso-8601=minutes) + git commit --date "$minutes_ago" -vm 'test + +/spend 7m' + + # 2.b. Check that adding that commit went well + + run "${git_spend}" sum --since HEAD~1 --minutes + assert_success + assert_output '7' + + run git log HEAD~1..HEAD + assert_success + assert_output --partial '/spend 7m' + + # 3.a. Add another commit, this time using "/spend HEAD" + + touch some_other_file && git add some_other_file + git commit -vm 'test: another + +/spend HEAD' + + # 3.b. Hopefully the rewrite hook did its job + + run git log HEAD~1..HEAD + assert_success + assert_output --partial '/spend 5m' +} + +# --- + +setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + export TZ="Europe/Paris" + + TESTS_DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" + PROJECT_DIR="$( dirname "$TESTS_DIR" )" + COVERAGE_DIR="${PROJECT_DIR}/test-coverage" + git_spend="${PROJECT_DIR}/build/git-spend" + + cd "${PROJECT_DIR}" || exit + + if [ "$GIT_SPEND_COVERAGE" == "1" ] ; then + echo "Setting up coverage in ${COVERAGE_DIR}" + mkdir -p "${COVERAGE_DIR}" + export GOCOVERDIR=${COVERAGE_DIR} + git_spend="${git_spend}-coverage" + fi + + cp -R "${PROJECT_DIR}" "${TMP_FIXTURE_DIR}" + cd "${TMP_FIXTURE_DIR}" || exit + + git stash + git checkout tags/fixture-00 -b fixture-00 + echo "success: ignore the unable to rmdir warning above (benign)" + + git log > fixture-00.log + git log 0.1.0 > 0.1.0.log +} + +teardown() { + rm -rf $TMP_FIXTURE_DIR + rm -f fixture-00.log + rm -f 0.1.0.log +}