├── .github └── workflows │ ├── analysis.yml │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .simplecov ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYING ├── Gemfile ├── README.md ├── README_en.md ├── Rakefile ├── bin ├── buchungsstreber ├── rubocop_changes └── simplecov_18_17 ├── buchungsstreber-tui.gemspec ├── buchungsstreber.gemspec ├── doc ├── buch_format.md ├── development.md ├── generatoren.md ├── resolver.md ├── rubygems.md ├── tutorial.md └── yaml_format.md ├── docker └── Dockerfile ├── example.buchungen.yml ├── example.config.yml ├── lib ├── buchungsstreber.rb └── buchungsstreber │ ├── aggregator.rb │ ├── cli │ ├── app.rb │ ├── runner.rb │ └── tui.rb │ ├── config.rb │ ├── entry.rb │ ├── generator.rb │ ├── generator │ ├── dailydoings.rb │ ├── git.rb │ ├── mail.rb │ ├── mention.rb │ ├── ncalcli.rb │ ├── redmine.rb │ ├── redminetimeentries.rb │ └── xchat.rb │ ├── i18n │ ├── buchungsstreber.pot │ ├── config.rb │ ├── de.po │ ├── en-029.po │ └── en.po │ ├── parser.rb │ ├── parser │ ├── buch_timesheet.rb │ ├── linebased_utils.rb │ └── yaml_timesheet.rb │ ├── redmine_api.rb │ ├── redmines.rb │ ├── resolver.rb │ ├── resolver │ ├── redmines.rb │ ├── regexp.rb │ └── templates.rb │ ├── utils.rb │ ├── validator.rb │ ├── version.rb │ └── watcher.rb ├── renovate.json └── spec ├── aggregator_spec.rb ├── buchungsstreber_spec.rb ├── cli_spec.rb ├── config_spec.rb ├── examples ├── aggregatable.B ├── aggregatable.yml ├── invalid.B ├── invalid.yml ├── invalid_time.B ├── invalid_time.yml ├── test.B └── test.yml ├── generator ├── dailydoings_spec.rb ├── git_spec.rb ├── mail_spec.rb ├── mention_spec.rb ├── ncalcli_spec.rb ├── redmine_spec.rb ├── redminetimeentries_spec.rb └── xchat_spec.rb ├── parser ├── buch_timesheet_spec.rb ├── timesheet_examples.rb └── yaml_timesheet_spec.rb ├── parser_spec.rb ├── redmine_api_spec.rb ├── resolver └── regexp_spec.rb ├── spec_helper.rb ├── support └── custom_expectations │ └── write_expectations.rb ├── utils_spec.rb └── validator_spec.rb /.github/workflows/analysis.yml: -------------------------------------------------------------------------------- 1 | name: Analysis 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | analyze: 11 | name: "SonarCloud" 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout source code 16 | uses: actions/checkout@v4 17 | with: 18 | # This is needed, so SonarCloud can analyze correctly the times when regressions were introduced 19 | fetch-depth: 0 20 | 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: 3.1 25 | 26 | - name: Recreate Ruby Bundler cache 27 | uses: actions/cache@v4 28 | with: 29 | path: vendor/bundle 30 | key: ${{ runner.os }}-analysis-bundler-${{ hashFiles('**/Gemfile*') }} 31 | restore-keys: | 32 | ${{ runner.os }}-analysis-bundler- 33 | 34 | - name: Install dependencies 35 | run: bundle install --path ./vendor/bundle 36 | 37 | - name: Run tests 38 | run: bundle exec rspec 39 | 40 | - name: Run rubocop 41 | run: ./bin/rubocop_changes 42 | continue-on-error: true 43 | 44 | - name: Prepare simplecov report 45 | run: ./bin/simplecov_18_17 coverage/.resultset.json > coverage/.resultset17.json 46 | 47 | - name: SonarCloud Scan 48 | uses: sonarsource/sonarcloud-github-action@master 49 | with: 50 | args: > 51 | -Dsonar.organization=synyx 52 | -Dsonar.projectKey=com.github.synyx:buchungsstreber 53 | -Dsonar.ruby.coverage.reportPaths=/github/workspace/coverage/.resultset17.json 54 | -Dsonar.sources=/github/workspace/bin/,/github/workspace/lib/ 55 | -Dsonar.tests=/github/workspace/spec/ 56 | -Dsonar.coverage.exclusions=**/tui.rb 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Buchungsstreber: Release Pipeline" 3 | 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | publish: 11 | name: Publish artifacts 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout project 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 3.1 21 | 22 | - name: Recreate Ruby Bundler cache 23 | uses: actions/cache@v4 24 | with: 25 | path: vendor/bundle 26 | key: ${{ runner.os }}-bundler-${{ hashFiles('**/Gemfile*') }} 27 | restore-keys: | 28 | ${{ runner.os }}-bundler- 29 | 30 | - name: Install dependencies 31 | run: bundle install --path ./vendor/bundle 32 | 33 | - name: Run tests 34 | run: bundle exec rspec 35 | 36 | - name: Publish artifacts to Nexus 37 | run: | 38 | mkdir -p ~/.gem 39 | AUTH=$(echo -n "$NEXUS_USER:$NEXUS_PASS" | base64) 40 | /bin/echo -e ":url: $NEXUS_URL\n:authorization: Basic $AUTH\n" > ~/.gem/nexus 41 | bundle exec rake build 42 | bundle exec gem nexus pkg/buchungsstreber-*.gem 43 | env: 44 | NEXUS_PASS: ${{ secrets.NEXUS_PASS }} 45 | NEXUS_URL: ${{ secrets.NEXUS_URL }} 46 | NEXUS_USER: ${{ secrets.NEXUS_USER }} 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: "Build & Test" 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | ruby: [2.7, 3.0, 3.3] 17 | 18 | steps: 19 | - name: Checkout source code 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 # for bin/rubocop_changes 23 | 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | 29 | - name: Recreate Ruby Bundler cache 30 | uses: actions/cache@v4 31 | with: 32 | path: vendor/bundle 33 | key: ${{ runner.os }}-bundler-${{ hashFiles('**/Gemfile*') }} 34 | restore-keys: | 35 | ${{ runner.os }}-bundler- 36 | 37 | - name: Install dependencies 38 | run: bundle install --path ./vendor/bundle 39 | 40 | - name: Run tests 41 | run: bundle exec rspec 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | archive/ 3 | config.yml 4 | 5 | # Bundler / RSpec 6 | .bundle 7 | vendor/ 8 | coverage 9 | .rspec_status 10 | pkg/ 11 | tmp/ 12 | .gem_rbs* 13 | 14 | # Translation 15 | lib/buchungsstreber/i18n/*.mo 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --order random 4 | --warnings 5 | --require spec_helper 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | TargetRubyVersion: 2.5 4 | NewCops: enable 5 | 6 | Layout/LineLength: 7 | Max: 120 8 | 9 | Style/TrailingCommaInHashLiteral: 10 | Enabled: false 11 | 12 | Style/ParallelAssignment: 13 | Enabled: false 14 | 15 | Style/FrozenStringLiteralComment: 16 | Enabled: false 17 | 18 | Style/Documentation: 19 | Enabled: false 20 | 21 | Style/StringLiterals: 22 | Enabled: false 23 | 24 | Style/ConditionalAssignment: 25 | Enabled: false 26 | 27 | Style/MultipleComparison: 28 | Enabled: false 29 | 30 | Layout/FirstHashElementIndentation: 31 | Enabled: false 32 | 33 | Style/SpecialGlobalVars: 34 | Enabled: false 35 | 36 | Style/RegexpLiteral: 37 | Enabled: false 38 | 39 | Style/FormatString: 40 | Enabled: false 41 | 42 | Style/StderrPuts: 43 | Enabled: false 44 | 45 | Style/PerlBackrefs: 46 | Enabled: false 47 | 48 | Style/ClassAndModuleChildren: 49 | Enabled: false 50 | 51 | Style/FormatStringToken: 52 | Enabled: false 53 | 54 | Metrics/AbcSize: 55 | Enabled: false 56 | Metrics/MethodLength: 57 | Enabled: false 58 | Metrics/CyclomaticComplexity: 59 | Enabled: false 60 | Metrics/PerceivedComplexity: 61 | Enabled: false 62 | Metrics/BlockLength: 63 | Enabled: false 64 | Metrics/ClassLength: 65 | Enabled: false 66 | 67 | # Disable this, we are using the constants for Aruba/InProcess testing 68 | Style/GlobalStdStream: 69 | Enabled: false 70 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.command_name "buchungsstreber" 2 | SimpleCov.root(__dir__) 3 | 4 | if SimpleCov.respond_to? :enable_for_subprocesses 5 | SimpleCov.enable_for_subprocesses false 6 | end 7 | 8 | SimpleCov.configure do 9 | filters.clear 10 | load_profile 'bundler_filter' 11 | load_profile 'hidden_filter' 12 | add_filter 'spec/support' 13 | add_filter do |src| 14 | !(src.filename =~ /^#{SimpleCov.root}/) 15 | end 16 | 17 | # Changed Files in Git Group 18 | # @see http://fredwu.me/post/35625566267/simplecov-test-coverage-for-changed-files-only 19 | untracked = `git ls-files --exclude-standard --others -z` 20 | unstaged = `git diff --name-only -z` 21 | staged = `git diff --name-only --cached -z` 22 | changed_filenames = [untracked, unstaged, staged].map { |x| x.split("\x0") }.flatten 23 | 24 | unless changed_filenames.empty? 25 | add_group 'Changed' do |source_file| 26 | changed_filenames.find { |x| source_file.filename.end_with?(x) } 27 | end 28 | end 29 | 30 | add_group 'Specs', 'spec' 31 | add_group 'Libraries', 'lib' 32 | end 33 | 34 | SimpleCov.start 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | ## v2.12.2 5 | 6 | * Update for Ruby >= 3.3 7 | * Replace underlying TUI library (`ncursesw` -> `curses`) 8 | 9 | ## v2.11.1 10 | 11 | * Fix time steps for non quarter hour ([#125](https://github.com/synyx/buchungsstreber/pull/125)) 12 | 13 | ## v2.10.0 14 | 15 | * Add dockerfile for containerized buchungsstreber 16 | * Introduce DailyDoings ([#107](https://github.com/synyx/buchungsstreber/issues/107)) 17 | * Add creation time to `add` command ([#115](https://github.com/synyx/buchungsstreber/issues/115)) 18 | * Fix handling of empty YAML buchungen file 19 | * Fix search path for config file 20 | 21 | ## v2.9.1 22 | 23 | * Add Redmine Time Entry generator ([#101](https://github.com/synyx/buchungsstreber/issues/101)) 24 | * Fix potential data loss on generating entries ([#94](https://github.com/synyx/buchungsstreber/issues/94)) 25 | 26 | ## v2.8.2 27 | 28 | * Fix "Messy" YAML file when using the `add` feature ([#84](https://github.com/synyx/buchungsstreber/issues/84)) 29 | 30 | ## v2.8.1 31 | 32 | * Make sure entries get added in order ([#83](https://github.com/synyx/buchungsstreber/issues/83)) 33 | 34 | ## v2.8.0 35 | 36 | * Add time entries or notes from command line ([#11](https://github.com/synyx/buchungsstreber/issues/11)) 37 | * Template handling for Buch format ([#48](https://github.com/synyx/buchungsstreber/issues/48)) 38 | * Expand path configuration for Git generator 39 | * Various housekeeping actions 40 | * Introduce [Renovate](https://togithub.com/renovatebot/renovate) 41 | 42 | ## v2.7.0 43 | 44 | * Fix confusing internationalized confirmation of saving entries in german 45 | * Prefer VISUAL environment variable for opening editor 46 | * Documentation enhancements 47 | * Various generator fixes 48 | 49 | ## v2.6.2 50 | 51 | * Remove usage of `/users/current` API endpoint to avoid issues with 52 | certain Redmine installations 53 | 54 | ## v2.6.1 55 | 56 | * Bugfix regarding entries not getting aggregated in `buchen` action 57 | 58 | ## v2.6.0 59 | 60 | Note that this release contains backwards-compatibility breaking changes. 61 | 62 | * Remove deprecated `gcalcli` generator 63 | * TUI behavioural change: Using the enter key after committing the times will 64 | now switch to the next day, escape will stay on current day 65 | * The curses library used for the TUI was replaced due to instability 66 | * A separate `buchungsstreber-tui` gem will be published for easier TUI usage 67 | 68 | ## v2.5.0 69 | 70 | * Add color configuration for redmines 71 | * Make minimum time per entry configurable 72 | * Minor and major bugfixes while moving from GitLab to GitHub as platform 73 | * Add an english README file 74 | * Add general documentation for a more open development and contribution guidelines 75 | 76 | ## v2.4.0 77 | 78 | * ncalcli generator can now filter shown entries 79 | * behavioral change: regexp resolver will now override non-empty fields in entries 80 | 81 | ## v2.3.1 82 | 83 | * Relax validation for duplicated entries 84 | 85 | ## v2.3.0 86 | 87 | * TUI gets a `today` argument 88 | `buchungsstreber watch today` 89 | * Documentation enhancements 90 | * Performance enhancements/reduced API usage for TUI 91 | 92 | ## v2.2.6 93 | 94 | * Fix generation in YAML format if file contains a single newline 95 | 96 | ## v2.2.5 97 | 98 | * Bring YAML Format in line regarding generators 99 | 100 | ## v2.2.4 101 | 102 | * Bugfix release regarding translations 103 | 104 | ## v2.2.3 105 | 106 | * There is no release v2.2.3 107 | 108 | ## v2.2.2 109 | 110 | * Revise TUI keys for exiting sub-window (enter, space, backspace) 111 | 112 | ## v2.2.1 113 | 114 | * Add german and english translations 115 | 116 | ## v2.2.0 117 | 118 | * Skip entering times already available on Redmine server 119 | * Revise arrow keys in TUI 120 | * Add `ncalcli` generator 121 | * Bugfixes 122 | 123 | ## v2.1.x 124 | 125 | * Enhance TUI with curses for entering hours 126 | * Aggregate time entries 127 | * Bugfixes 128 | 129 | ## v2.0.x 130 | 131 | * Unstable development release 132 | * Add TUI with curses for displaying hours 133 | * Refactor application for extensibility 134 | * Add concept of generators (generate entries if none exist) 135 | * Add concept of resolvers (revise entries) 136 | * Add CLI with more commands 137 | * Add built-in initialization if no config exists 138 | * Add built-in help on first-use 139 | 140 | ## v1.5.0 141 | 142 | * Make installable as a ruby gem 143 | * Provide commandline app `buchungsstreber` 144 | 145 | # v1.2.0 146 | 147 | * Add configuration option for working hours (8h by default) 148 | ```yaml 149 | hours: 8 150 | ``` 151 | 152 | ## v1.1.0 153 | 154 | * Support time spans as issue time (rounded up to quarters of an hour) 155 | 156 | ## v1.0.0 157 | 158 | * Initial release 159 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thanks for reading this, we're glad if this piece of software is useful to you 4 | and you want to add to its value. 5 | 6 | ## How can I contribute? 7 | 8 | ### Documentation 9 | 10 | The documentation is always in need of help. Whether you are fixing typos or 11 | wordings or create new documents on how to use the `buchungsstreber`, 12 | everything is welcome. 13 | 14 | ### Code 15 | 16 | When you are creating or changing code, it is nice if you also create a test 17 | with `rspec` so the behaviour is documented. 18 | 19 | There are `rubocop` rules regarding style, but those are not hard and fast. 20 | 21 | ### Bugs 22 | 23 | If you're already using the software, it is helpful if you report problems 24 | via the GitHub issue tracker. 25 | Before submitting bug reports, have a look at the existing ones. There 26 | might be one you can comment on. 27 | 28 | For reporting, try to: 29 | 30 | * add a short descriptive title 31 | * add a way to reproduce the behaviour 32 | * best with versions of OS, Ruby and `buchungsstreber` 33 | * add screenshots if applicable 34 | * add the output of the app with `--debug` enabled 35 | 36 | ## Styleguides 37 | 38 | ### Git commits 39 | 40 | Try to write somewhat clean changes. To cite [Tom Lord][tla]: 41 | 42 | > #### Using commit Well -- The Idea of a Clean Changeset 43 | > 44 | > When you commit a set of changes, it is generally "best practice" to 45 | > make sure you are creating a clean changeset. 46 | > 47 | > A clean changeset is one that contains only changes that are all 48 | > related and for a single purpose. For example, if you have several 49 | > bugs to fix, or several features to add, try not to mix those changes 50 | > up in a single commit . 51 | 52 | [tla]: https://www.gnu.org/software/gnu-arch/tutorial-old/exploring-changesets.html#Exploring_Changesets 53 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2020 The Original Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in buchungsstreber.gemspec 4 | gemspec :name => 'buchungsstreber' 5 | 6 | group :tui, optional: true do 7 | # Keep in sync with buchungsstreber-tui dependencies 8 | gem 'curses', '1.5.0' 9 | gem 'rb-inotify', '0.11.1' 10 | end 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Buchungsstreber (⌐⊙_⊙) 2 | ====================== 3 | 4 | ![CI](https://github.com/synyx/buchungsstreber/workflows/CI/badge.svg) 5 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=com.github.synyx%3Abuchungsstreber&metric=coverage)](https://sonarcloud.io/dashboard?id=com.github.synyx%3Abuchungsstreber) 6 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=com.github.synyx%3Abuchungsstreber&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=com.github.synyx%3Abuchungsstreber) 7 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=com.github.synyx%3Abuchungsstreber&metric=security_rating)](https://sonarcloud.io/dashboard?id=com.github.synyx%3Abuchungsstreber) 8 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=com.github.synyx%3Abuchungsstreber&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.github.synyx%3Abuchungsstreber) 9 | 10 | Lost? -> [English README][english]. 11 | 12 | Der Buchungsstreber hilft beim konsistenten und zeitnahen Buchen in [Redmine][redmine], indem er 13 | in einer Textdatei gepflegte Buchungen automatisch in ein oder mehrere Redmine-Systeme überträgt. 14 | 15 | [redmine]: https://www.redmine.org 16 | [english]: README_en.md 17 | 18 | * [Schnacken](#schnacken) 19 | * [Voraussetzungen](#voraussetzungen) 20 | * [Installation](#installation) 21 | * [Konfiguration](#konfiguration) 22 | * [Nutzung](#nutzung) 23 | * [Terminal User Interface](#terminal-user-interface) 24 | * [Entwicklung](#entwicklung) 25 | 26 | Schnacken 27 | --------- 28 | 29 | Fragen? Hilfe notwendig? Einfach nur mal Schnacken? 30 | 31 | * Matrix: [#buchungsstreber:synyx.de](https://matrix.to/#/!BxFxbjMxhzwOlFxvMm:synyx.de/) 32 | * E-Mail: [buchungsstreber@synyx.de](mailto:buchungsstreber@synyx.de) 33 | * [Contribution Guidelines][contributing] 34 | 35 | [contributing]: CONTRIBUTING.md 36 | 37 | Voraussetzungen 38 | --------------- 39 | 40 | - Ruby 2.x/3.x 41 | - bundler (fuer Entwicklung) 42 | - ncursesw (fuer buchungsstreber-tui) (Kompatibel mit Ruby Version) 43 | - schlechte Buchungsmoral 44 | 45 | Installation 46 | ------------ 47 | 48 | 1. `gem install buchungsstreber` (Mit eingerichteter [Paketquelle][rubygems]) 49 | 50 | [rubygems]: doc/rubygems.md 51 | 52 | Oder via git Repository: 53 | 54 | 1. Repository auschecken 55 | 2. Ruby-Gems installieren: `bundle install` 56 | 57 | Konfiguration 58 | ------------ 59 | 60 | 1. Initialisierung durchfuehren lassen via 61 | `buchungsstreber init` 62 | 63 | Oder 64 | 65 | 1. Konfigurationspfad für Buchungstreber erstellen: 66 | `mkdir ~/.config/buchungsstreber` 67 | 68 | 2. Config-Datei anhand der [Beispiel-Config](example.config.yml) erstellen. 69 | 70 | Mindestens die eigenen Redmine-API-Keys eintragen, ggf. auch den Pfad zur 71 | Buchungs-Datei `timesheet_file` und (je nach Arbeitsweise) den Archiv-Ordner 72 | `archive_path` anpassen: `buchungsstreber config` (edit 73 | `~/.config/buchungsstreber/config.yml`). 74 | 75 | Nutzung 76 | ------- 77 | 78 | Bei erstmaliger Anwendung hilft das [TUTORIAL](./doc/tutorial.md). 79 | 80 | Buchungen werden als Plaintext erfasst, vgl. [Beispiel](example.buchungen.yml). Jede Zeile entspricht dabei einer Buchung. 81 | Eine "Datums-Überschrift" spezifiert das Datum der darunter folgenden Buchungen. 82 | 83 | Eine Buchungs-Zeile hat dabei immer folgendes Format (getrennt durch Tabs oder Leerzeichen): 84 | ```yaml 85 | - [Zeit] [Aktivität] [Ticket-Nr.] [Beschreibung] 86 | ``` 87 | 88 | ### Beispiel: 89 | ```yaml 90 | 2019-01-01: 91 | - 1.5 Orga 12345 Nachbereitung 92 | ``` 93 | In diesem Fall würden für den *01.01.2019* eineinhalb Stunden auf das Ticket #12345 gebucht. 94 | Die Aktivität wäre dabei "Orga" und die Beschreibung "Nachbereitung". 95 | 96 | Vollstaendige Beschreibungen fuer: 97 | 98 | * [YAML Format](./doc/yaml_format.md) 99 | * [Buch Format](./doc/buch_format.md) 100 | 101 | ### Let's buch it 102 | 103 | Sobald ein paar Buchungen eingetragen sind, sollte der Buchungsstreber einfach 104 | gestartet werden können durch: `buchungsstreber` 105 | 106 | Keine Sorge, der Buchungsstreber validiert erst einmal die Einträge in der 107 | Buchungs-Datei und bucht nicht direkt los. 108 | 109 | 110 | ## Terminal User Interface 111 | 112 | Mit curses ist eine Oberflaeche vorhanden, welche zur Ueberpruefung von 113 | Buchungen sowie zum abschliessenden Buchen verwendet werden kann. 114 | 115 | ```shell script 116 | buchungsstreber watch today 117 | buchungsstreber watch 2020-09-01 118 | ``` 119 | 120 | Benoetigt werden hierzu noch Rubygem Abhaengigkeiten: 121 | 122 | * `curses` 123 | * `listen` oder `rb-inotify` oder `filewatcher` 124 | 125 | Bedienungsanleitung erreichbar mit `h`. 126 | 127 | 128 | Entwicklung 129 | ----------- 130 | 131 | * [CONTRIBUTING](./CONTRIBUTING.md) 132 | * [Development Guide](./doc/development.md) 133 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | Buchungsstreber (⌐⊙_⊙) 2 | ====================== 3 | 4 | The Buchungsstreber (translated roughly as 'time entry nerd') helps you adding time entries to 5 | [Redmine][redmine]. It enables writing a simple text file and will take care of adding those 6 | entries to one or more [Redmine][redmine] instances. 7 | 8 | [redmine]: https://www.redmine.org 9 | 10 | * [Help](#help) 11 | * [Requirements](#requirements) 12 | * [Installation](#installation) 13 | * [Configuration](#configuration) 14 | * [Usage](#usage) 15 | * [Terminal User Interface](#terminal-user-interface) 16 | * [Development](#development) 17 | 18 | Help 19 | ---- 20 | 21 | Questions? Help needed? Simply wanna talk? 22 | 23 | * Matrix: [#buchungsstreber:synyx.de](https://matrix.to/#/!BxFxbjMxhzwOlFxvMm:synyx.de/) 24 | * E-Mail: [buchungsstreber@synyx.de](mailto:buchungsstreber@synyx.de) 25 | * [Contribution Guidelines][contributing] 26 | 27 | [contributing]: CONTRIBUTING.md 28 | 29 | Requirements 30 | --------------- 31 | 32 | - Ruby 2.x/3.x 33 | - bundler (for development) 34 | - ncursesw (for buchungsstreber-tui) (compatible to ruby version) 35 | - bad time entry moral 36 | 37 | Installation 38 | ------------ 39 | 40 | 1. `gem install buchungsstreber` (With configured [package resource][rubygems]) 41 | 42 | [rubygems]: doc/rubygems.md 43 | 44 | Or via git repository: 45 | 46 | 1. Check out repository 47 | 2. Install needed gems: `bundle install` 48 | 49 | Configuration 50 | ------------- 51 | 52 | 1. Initialize configuration with 53 | `buchungsstreber init` 54 | 55 | Or 56 | 57 | 1. Create configuration path: 58 | `mkdir ~/.config/buchungsstreber` 59 | 60 | 2. Create config using the [example config](example.config.yml). 61 | 62 | Add at least the own redmine API key and the path to your time entry file 63 | `timesheet_file`. 64 | Depending on the usage you also need to configure the archive folder 65 | `archive_path`. 66 | 67 | You can edit the configuration using `buchungsstreber config` (or editing 68 | `~/.config/buchungsstreber/config.yml`). 69 | 70 | Usage 71 | ------- 72 | 73 | You can visit the [TUTORIAL](./doc/tutorial.md) if using it the first time. 74 | 75 | Time entries are done as plaintext files, see [Example](example.buchungen.yml). Each 76 | line represents one time entry. 77 | 78 | A line is done according to this specification, each token separated by spaces or tabs: 79 | ```yaml 80 | - [Zeit] [Aktivität] [Ticket-Nr.] [Beschreibung] 81 | ``` 82 | 83 | ### Example: 84 | ```yaml 85 | 2019-01-01: 86 | - 1.5 Orga 12345 Nachbereitung 87 | ``` 88 | In this case, there would be a time entry on the 1st January for one and a half 89 | hours on ticket #12345. 90 | The activity would be organizational in nature and has a text after that. 91 | 92 | Full descriptions for the two current plaintext formats: 93 | 94 | * [YAML Format](./doc/yaml_format.md) 95 | * [Buch Format](./doc/buch_format.md) 96 | 97 | ### Let's buch it 98 | 99 | As soon as there is at least one time entry, the Buchungsstreber can be run 100 | via `buchungsstreber`. 101 | 102 | Don't worry, the Buchungsstreber will validate the entries first, and will 103 | not enter completely bogus times. 104 | 105 | ## Terminal User Interface 106 | 107 | There is a curses based interface, which can be used to validate the entries 108 | and to enter the times into Redmine instances. 109 | 110 | ```shell script 111 | buchungsstreber watch today 112 | buchungsstreber watch 2020-09-01 113 | ``` 114 | 115 | To use the TUI interface, there are some more gem requirements: 116 | 117 | * `curses` 118 | * `listen` oder `rb-inotify` oder `filewatcher` 119 | 120 | You can reach a help interface by pressing `h`. 121 | 122 | Development 123 | ----------- 124 | 125 | * [CONTRIBUTING](./CONTRIBUTING.md) 126 | * [Development Guide](./doc/development.md) 127 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | CLOBBER.include "pkg" 3 | 4 | require "bundler/gem_helper" 5 | Bundler::GemHelper.install_tasks name: 'buchungsstreber' 6 | Bundler::GemHelper.install_tasks name: 'buchungsstreber-tui' 7 | 8 | desc 'Regenerate Translation Template' 9 | task :xgettext do 10 | lib = File.expand_path('lib', __dir__) 11 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 12 | require 'buchungsstreber/version' 13 | 14 | args = [ 15 | '--copyright-holder=Jonathan Buch ', 16 | "--package-version=#{Buchungsstreber::VERSION}", 17 | '--package-name=BUCHUNGSSTREBER', 18 | '--msgid-bugs-address=jbuch@synyx.de', 19 | '--sort-by-msgid', 20 | '--output=./lib/buchungsstreber/i18n/buchungsstreber.pot', 21 | Dir.glob('lib/buchungsstreber/cli/*.rb'), 22 | ].flatten 23 | system('rxgettext', *args) 24 | end 25 | 26 | require "rspec/core/rake_task" 27 | RSpec::Core::RakeTask.new(:spec) 28 | 29 | task :default => :spec 30 | -------------------------------------------------------------------------------- /bin/buchungsstreber: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'buchungsstreber/cli/runner' 4 | 5 | if Gem.win_platform? 6 | ENV['THOR_SHELL'] ||= 'Color' 7 | ENV['RUBYOPT'] ||= '-E utf-8' 8 | ENV['EDITOR'] ||= 'notepad' 9 | end 10 | 11 | require 'i18n' 12 | require_relative '../lib/buchungsstreber/i18n/config' 13 | I18n::Backend::Simple.include(I18n::Backend::Gettext) 14 | I18n.load_path << Dir[File.join(__dir__, '../lib/buchungsstreber/i18n/*.po')] 15 | I18n.default_locale = :de 16 | I18n.config = I18n::Env::Config.new 17 | I18n.backend.send(:init_translations) 18 | 19 | Buchungsstreber::CLI::Runner.new(ARGV.dup).execute! 20 | -------------------------------------------------------------------------------- /bin/rubocop_changes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This will filter a concatenated input of rubocop and git diff to only output rubocop 4 | # issues for ones in the actual changes. 5 | 6 | target_branch = ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['GITHUB_BASE_REF'] || 'main' 7 | 8 | RUBOCOP = < app2, 15 | 'timestamp' => app['timestamp'], 16 | } 17 | out 18 | end 19 | 20 | puts JSON.dump(coverage17) 21 | -------------------------------------------------------------------------------- /buchungsstreber-tui.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'buchungsstreber/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'buchungsstreber-tui' 7 | spec.version = Buchungsstreber::VERSION 8 | spec.licenses = ['MIT'] 9 | spec.authors = ['jbuch', 'dgrammlich'] 10 | spec.email = ['jonathan.buch@gmail.com', 'grammlich@synyx.de'] 11 | 12 | spec.summary = %q{Enables timely timekeeping} 13 | spec.description = "Enriches buchungsstreber with TUI." 14 | spec.homepage = 'https://buchungsstreber.synyx.de' 15 | 16 | spec.metadata['allowed_push_host'] = 'https://nexus.synyx.de/content/repositories/gems/' 17 | 18 | spec.metadata['homepage_uri'] = spec.homepage 19 | spec.metadata['source_code_uri'] = 'https://gitlab.synyx.de/synyx/buchungsstreber' 20 | spec.metadata['changelog_uri'] = 'https://gitlab.synyx.de/synyx/buchungsstreber/CHANGELOG.md' 21 | spec.files = [] 22 | 23 | spec.add_dependency 'buchungsstreber', Buchungsstreber::VERSION 24 | spec.add_dependency 'curses', '~> 1.5' 25 | spec.add_dependency 'rb-inotify', '~> 0.10.0' 26 | end 27 | -------------------------------------------------------------------------------- /buchungsstreber.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'buchungsstreber/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'buchungsstreber' 7 | spec.version = Buchungsstreber::VERSION 8 | spec.licenses = ['MIT'] 9 | spec.authors = ['fhfet', 'jbuch', 'grammlich', 'heib'] 10 | spec.email = ['heft@synyx.de', 'jonathan.buch@gmail.com', 'grammlich@synyx.de', 'heib@synyx.de'] 11 | 12 | spec.summary = %q{Enables timely timekeeping.} 13 | spec.description = "Streber." 14 | spec.homepage = 'https://buchungsstreber.synyx.de' 15 | 16 | spec.metadata['allowed_push_host'] = 'https://nexus.synyx.de/content/repositories/gems/' 17 | 18 | spec.metadata['homepage_uri'] = spec.homepage 19 | spec.metadata['source_code_uri'] = 'https://gitlab.synyx.de/synyx/buchungsstreber' 20 | spec.metadata['changelog_uri'] = 'https://gitlab.synyx.de/synyx/buchungsstreber/CHANGELOG.md' 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 25 | excludes = %r{^(test|spec|features|\.|Gem|Rake|rubocop|simplecov|doc)/} 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(excludes) } 27 | end 28 | spec.bindir = 'bin' 29 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | spec.add_dependency 'i18n', '~> 1.14.0' 33 | spec.add_dependency 'thor', '~> 1.3' 34 | # add racc, not because it is needed directly, but because things just happen to be that way. 35 | spec.add_dependency 'racc' 36 | 37 | spec.add_development_dependency 'aruba', '~> 2.3' 38 | spec.add_development_dependency 'bundler', '~> 2.0' 39 | spec.add_development_dependency 'gettext', '~> 3.5' 40 | spec.add_development_dependency 'nexus', '~> 1.5' 41 | spec.add_development_dependency 'rake', '~> 13.2' 42 | spec.add_development_dependency 'rspec', '~> 3.13' 43 | spec.add_development_dependency 'rubocop', '~> 1.75' 44 | spec.add_development_dependency 'simplecov', '~> 0.22.0' 45 | spec.add_development_dependency 'webmock', '~> 3.25' 46 | end 47 | -------------------------------------------------------------------------------- /doc/buch_format.md: -------------------------------------------------------------------------------- 1 | # Buch Buchungsformat 2 | 3 | ### Standard-Form 4 | 5 | ``` 6 | Datum 7 | 8 | []# 9 | ``` 10 | 11 | #### Beispiel 12 | 13 | ``` 14 | 2019-01-01 15 | 16 | #1234 1.5 Daily Maint Daily 17 | s#25888 0.5 Orga Buchungsdext 18 | ``` 19 | 20 | ### Zeitangaben 21 | 22 | Es sind Stunden-Angaben (`1.25`, `1:15`) verwendbar. 23 | 24 | ``` 25 | #1234 1.25 Orga Meeting 26 | #1234 1:15 Orga Meeting 27 | ``` 28 | 29 | ### Zeitgranularitaet 30 | 31 | Zeiten werden aufgerundet. Die mindestens zu buchende Zeit kann über die 32 | Konfiguration `minimum_time` konfiguriert werden. Ist beispielsweise eine 33 | Viertelstunde konfiguriert 34 | 35 | ``` 36 | minimum_time: 0.25 37 | ``` 38 | 39 | wird 40 | 41 | ``` 42 | #1234 1:10 Orga Meeting 43 | ``` 44 | 45 | gebucht als 46 | 47 | ``` 48 | #1234 1.25 Orga Meeting 49 | ``` 50 | 51 | ## Vim Highlighting 52 | 53 | `~/.vim/syntax/buchungen.vim` 54 | 55 | ```viml 56 | " Vim syntax file 57 | " Language: Redmine Buchungen 58 | " Maintainer: Jonathan Buch 59 | " Latest Revision: 2016 60 | 61 | if version < 600 62 | syntax clear 63 | elseif exists("b:current_syntax") 64 | finish 65 | endif 66 | 67 | syn match redDate /\d\d\d\d-\d\d-\d\d/ 68 | syn keyword redTag GEBUCHT TODO XXX 69 | syn match redDate /KW\s*\d\d*/ 70 | 71 | " Rest is more fuzzy 72 | syn case ignore 73 | 74 | syn keyword redActivity contained admin analyse daily weekly desing doku einarb einarbeitung schulung enwicklung dev solo pair entw supp maint maintenance support orga planning plan retro review workshop 75 | syn match redTicket /#\d\d*/ contained nextgroup=redHours skipwhite 76 | syn match redTicket /[a-z]#\d\d*/ contained nextgroup=redHours skipwhite 77 | syn match redHours /\s\@<=\d\d*\.\@!/ contained 78 | syn match redHours /\d\d*\.\(25\|50\|5\|75\|0\)/ contained 79 | syn match redHours /\d\d*\:\(15\|30\|45\|0\)/ contained 80 | 81 | syn match redId /\[\d\d*\]/ contained 82 | 83 | syn match redError /\d\d*\.\(25\|50\|5\|75\|0\)\@!/ 84 | 85 | syn match redComment /%.*/ contains=redDate,redTag 86 | 87 | syn region redLine start=/^\(#\|[a-zA-Z]#\)/ end='$' oneline transparent contains=ALLBUT,redDate,redLine 88 | 89 | let b:current_syntax = "buchungen" 90 | 91 | hi def link redTicket Constant 92 | hi def link redHours Number 93 | hi def link redText Todo 94 | hi def link redComment Comment 95 | hi def link redDate Type 96 | hi def link redTag Tag 97 | hi def link redId Constant 98 | hi def link redActivity Directory 99 | hi def link redError Error 100 | ``` 101 | 102 | `~/.vim/ftdetect/buchungen.vim` 103 | 104 | ```viml 105 | au BufRead,BufNewFile *.B set filetype=buchungen 106 | autocmd FileType buchungen set noet|set ts=8|set sw=8|set sts=8 107 | ``` 108 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | * [Environment](#environment) 4 | * [Running tests](#running-tests) 5 | * [Debugging](#debugging) 6 | 7 | ## Environment 8 | 9 | To set up your environment we recommend to install Bundler. 10 | ```` 11 | gem install bundler 12 | ```` 13 | Now you can install all necessary dependencies and start `buchungsstreber`. 14 | 15 | ```` 16 | bundle install --with=tui 17 | bundle exec ./bin/buchungsstreber 18 | ```` 19 | 20 | ## Running tests 21 | 22 | You can run `rspec` with 23 | ```` 24 | bundle exec rspec 25 | ```` 26 | If you just want to execute a specific test run 27 | ```` 28 | bundle exec rspec spec/my_spec.rb 29 | ```` 30 | 31 | ## I18n 32 | 33 | The application is translated via `gettext`. 34 | 35 | Basically, any string surrounded by the underscore method (`_('foo')`) is 36 | meant to be localized. 37 | 38 | The template for localized strings is in `lib/i18n/buchungsstreber.pot` and 39 | can be updated by running `bundle exec rake xgettext`. You probably have 40 | to install `gettext` first. 41 | 42 | After the `.pot` file has been updated, the translations can also be 43 | updated. 44 | 45 | ### Current translations 46 | 47 | * `de`: German (primary language) 48 | * `de`: English (primary language) 49 | * `en-029`: Caribbean (English without Unicode characters) 50 | 51 | ## Debugging 52 | 53 | To get the full exception instead of only a little message use `--debug`. 54 | 55 | ``` 56 | buchungsstreber show --debug 2020-01-01 57 | ``` 58 | 59 | As for the TUI, it is built with curses, so it will most probably swallow 60 | all your exceptions and stacktraces. 61 | If you're using a Bash like Shell and you're using printf style debugging: 62 | 63 | ``` 64 | buchungsstreber watch --debug 2020-01-01 2>error.log 65 | ``` 66 | 67 | This will output the standard error to the given file. 68 | -------------------------------------------------------------------------------- /doc/generatoren.md: -------------------------------------------------------------------------------- 1 | # Generatoren 2 | 3 | Generatoren beziehen Daten von externen Programmen und generieren 4 | Daten in der Timesheet-Datei. Diese Einträge können manuell oder per 5 | [Resolver][resolver] in buchungskonforme Einträge verändert werden. 6 | 7 | [resolver]: resolver.md 8 | 9 | ## Importiere Nextcloud- / Owncloud-Termine per ncalcli 10 | 11 | ncalcli: [Installation und Dokumentation](https://github.com/BuJo/ncalcli) 12 | 13 | ### Konfiguration des Buchungsstrebers 14 | 15 | Alle Termine generieren: 16 | ```` 17 | generators: 18 | ncalcli: {} 19 | ```` 20 | 21 | Bestimmte Termine ignorieren, `ignore` ist ein regulärer Ausdruck: 22 | ```` 23 | generators: 24 | ncalcli: 25 | ignore: Hunderunde|Mittagspause 26 | ```` 27 | 28 | ## Buchungseinträge aus Git-Commits generieren 29 | 30 | Der Generator erzeugt Buchungsstreber-Einträge anhand von Git-Commits. 31 | Er betrachtet maximal 5 Ordner-Ebenen der konfigurierten Pfade. 32 | 33 | ### Konfiguration des Buchungsstrebers 34 | 35 | Repositories konfigurieren, die betrachtet werden sollen: 36 | ````yaml 37 | generators: 38 | git: 39 | :dirs: 40 | - ~/Projects/buchungsstreber 41 | ```` 42 | 43 | ## Buchungseinträge aus E-Mails generieren 44 | 45 | Der Generator basiert auf einem experimentellen, nicht unterstützen Skript `cmi` 46 | das E-Mails durchsucht und gewünschte Treffer wieder ausgibt. 47 | Der Generator erzeugt Buchungsstreber-Einträge anhand der gefilterten E-Mails. 48 | Welche Ausgabe vom Generator erwartet wird kann dem 49 | [dazugehörigen Spec](spec/generator/mail_spec.rb) entnommen werden. 50 | 51 | ### Konfiguration des Buchungsstrebers 52 | 53 | ````yaml 54 | generators: 55 | mail: {} 56 | ```` 57 | 58 | ## Buchungseinträge aus Erwähnungen in Redmine-Kommentaren generieren 59 | 60 | Der Generator basiert auf einem experimentellen, nicht unterstützen Skript `cmm` 61 | das alle Zeiteinträge in Redmine nach Schlagwörtern durchsucht und Einträge mit 62 | möglichen Erwähnungen ausgibt. 63 | Der Generator erzeugt Buchungsstreber-Einträge anhand der gefilterten Einträge. 64 | Welche Ausgabe vom Generator erwartet wird kann dem 65 | [dazugehörigen Spec](spec/generator/mention_spec.rb) entnommen werden. 66 | 67 | ### Konfiguration des Buchungsstrebers 68 | 69 | ````yaml 70 | generators: 71 | mention: {} 72 | ```` 73 | 74 | ## Buchungseinträge aus Erwähnungen in Redmine-Kommentaren generieren 75 | 76 | Der Generator basiert auf einem experimentellen, nicht unterstützen Skript `cmr` 77 | das Aktivitäten eines Benutzers aus Redmine heraussucht und zurückgibt. 78 | Der Generator erzeugt Buchungsstreber-Einträge anhand der ausgegebenen 79 | Aktivitäten. 80 | Welche Ausgabe vom Generator erwartet wird kann dem 81 | [dazugehörigen Spec](spec/generator/redmine_spec.rb) entnommen werden. 82 | 83 | ### Konfiguration des Buchungsstrebers 84 | 85 | ````yaml 86 | generators: 87 | redmine: {} 88 | ```` 89 | 90 | ## Buchungseinträge aus XChat-Logs generieren 91 | 92 | Der Generator basiert auf einem experimentellen, nicht unterstützen Skript `cmx` 93 | das [XChat](http://xchat.org/)-Logs einliest, diese nach Schlagwörtern filtert 94 | und wieder ausgibt. 95 | Der Generator erzeugt Buchungsstreber-Einträge anhand der gefilterten Logs. 96 | Welche Ausgabe vom Generator erwartet wird kann dem 97 | [dazugehörigen Spec](spec/generator/xchat_spec.rb) entnommen werden. 98 | 99 | ### Konfiguration des Buchungsstrebers 100 | 101 | ````yaml 102 | generators: 103 | xchat: {} 104 | ```` 105 | 106 | ## Täglich wiederkehrende Einträge generieren 107 | 108 | Der Generator erzeugt tägliche Buchungsstreber-Einträge ohne zeitliche 109 | Abhängigkeit. 110 | 111 | ````yaml 112 | generators: 113 | dailydoings: 114 | - activity: Adm 115 | issue: t7654 116 | text: Daily cleanup 117 | ```` 118 | -------------------------------------------------------------------------------- /doc/resolver.md: -------------------------------------------------------------------------------- 1 | # Resolver 2 | 3 | Resolver wandeln Daten aus der Timesheet-Datei in vorkonfigurierte Daten-Templates um. 4 | 5 | ## Buchungstemplates 6 | 7 | Statt einer vollständigen Buchung kann ein Template verwendet werden, sodass nur dessen 8 | Namen in der Buchung angegeben werden muss. 9 | 10 | ```` 11 | templates: 12 | Frühstück: 13 | activity: Orga 14 | issue: S6325 15 | text: Frühstück 16 | ```` 17 | Die Buchung dazu sieht dann so aus: 18 | ```` 19 | 10:45-12:00 Frühstück 20 | ```` 21 | 22 | ## Match per regulärem Ausdruck 23 | 24 | Matcht der Timesheet-Eintrag den regulären Ausdruck, wird er durch die 25 | konfigurierten Daten ersetzt. Sinnvoll in Kombination mit einem 26 | [Generator][generatoren]. `re` ist der reguläre Ausdruck, unter `entry` wird der 27 | Buchungseintrag konfiguriert. 28 | 29 | [generatoren]: generatoren.md 30 | 31 | ```` 32 | resolvers: 33 | regexp: 34 | - re: bei Tiffany|Freitagsmeeting 35 | entry: 36 | activity: Orga 37 | issue: 6325 38 | redmine: S 39 | text: Frühstück 40 | - re: Daily 41 | entry: ... 42 | ```` 43 | Die generierte Buchung dazu sieht dann so aus: 44 | ```` 45 | 10:45-12:00 Orga S6325 Frühstück 46 | ```` 47 | -------------------------------------------------------------------------------- /doc/rubygems.md: -------------------------------------------------------------------------------- 1 | # Buchungsstreber via Rubygems 2 | 3 | ## Einmalige Einrichtung 4 | 5 | Falls man den buchungsstreber via Rubygems installiert hat, sollte die 6 | Paketquelle schon vorhanden sein. 7 | Falls nicht, kann diese mit folgenden Kommandos hinzugefuegt werden: 8 | 9 | ```shell script 10 | gem source --add https://nexus.synyx.de/content/repositories/gems/ 11 | ``` 12 | 13 | (Notiz: der `/` am Ende ist sehr wichtig.) 14 | In der `.gemrc` wird so eine neue source eingefuegt. 15 | 16 | ## Installation 17 | 18 | ```shell script 19 | gem install buchungsstreber 20 | ``` 21 | 22 | Je nach Installations-Art muessen potentiell `gem` Kommandos entweder mit 23 | `sudo` ausgefuehrt werden oder mit der option `--user-install`, um die Gems 24 | ins Homeverzeichnis zu installieren. 25 | Fuer `--user-install` siehe die FAQ fuer [User Install][userinstall]. 26 | 27 | [userinstall]: https://guides.rubygems.org/faqs/#user-install 28 | 29 | ### Windows 30 | 31 | * https://rubyinstaller.org/downloads/ 32 | Am besten als normaler Benutzer (nicht als Administrator) installieren (sonst Schmerzen). 33 | 34 | * Start Command Prompt with Ruby 35 | ```shell script 36 | gem install buchungsstreber 37 | ``` 38 | 39 | ## Update 40 | 41 | Update geht danach ueber: 42 | 43 | ```shell script 44 | gem update buchungsstreber 45 | ``` 46 | 47 | ## Neue Version releasen 48 | 49 | ```shell script 50 | vim lib/buchungsstreber/version.rb 51 | vim CHANGELOG.md 52 | git commit 53 | git tag v 54 | ``` 55 | 56 | Eine GitlabCI Pipeline reagiert auf Tags und uebernimmt die Paketerierung und 57 | Upload; falls per Hand released werden muss: 58 | 59 | ```shell script 60 | bundle exec rake build 61 | gem nexus pkg/buchungsstreber-.gem 62 | ``` 63 | -------------------------------------------------------------------------------- /doc/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | Vom Noob zum Buchungsstreber. 4 | 5 | ### Vorraussetzungen 6 | 7 | * Ruby ist installiert 8 | * Schlechte Buchungsmoral ist vorhanden 9 | * [Gem Paketquelle][rubygems] ist eingerichtet 10 | 11 | [rubygems]: rubygems.md 12 | 13 | ## Installation 14 | 15 | ``` 16 | $ gem install buchungsstreber 17 | ``` 18 | 19 | ## Erstmalige Einrichtung 20 | 21 | ``` 22 | $ buchungsstreber init 23 | Konfiguration in $HOME/.config/buchungsstreber/config.yml erstellt. 24 | 25 | Schritte zum friedvollen Buchen: 26 | * Config-Datei anpassen – mindestens die eigenen API-Keys eintragen. 27 | * Buchungsdatei oeffnen (siehe Konfig-Datei) 28 | * `buchungsstreber` ausfuehren 29 | ``` 30 | 31 | Gehe zu oder anderen Redmine-Instanzen, 32 | um API-Keys zu erstellen oder anzuzeigen. 33 | 34 | ``` 35 | $ buchungsstreber config 36 | # $EDITOR (oder vim) oeffnet sich, zumindest die API-Keys sind einzutragen. 37 | ``` 38 | 39 | ## Meine erste Buchung 40 | 41 | ``` 42 | $ buchungsstreber edit 43 | # $EDITOR (oder vim) oeffnet sich. 44 | ``` 45 | 46 | Nun sind im YAML-Format Buchungen anzulegen. Erstmal die Einrichtung des 47 | Buchungsstrebers buchen (Datum natuerlich anpassen): 48 | 49 | ```yaml 50 | 2019-06-17: 51 | - 0.5 Orga S34530 Einrichtung Buchungsstreber 52 | ``` 53 | 54 | Diese Buchung mal Validieren lassen (ohne gleich im Redmine anzulegen): 55 | 56 | ``` 57 | $ buchungsstreber 58 | BUCHUNGSSTREBER v1.5.1 59 | ~~~~~~~~~~~~~~~~~~~~~~ 60 | 61 | Buchungsübersicht: 62 | Wed: 0.5h @ Zeiten buchen : Einrichtung Buchungsstreber 63 | 64 | Zu buchende Stunden (2019-12-11 bis 2019-12-11): 65 | Wed: 0.5 66 | Buchungen in Redmine übernehmen? (j/N) 67 | N 68 | Abbruch 69 | ``` 70 | 71 | Dann den rest des Tages noch fertig buchen, und hinterher auf `J` druecken, 72 | um die Buchung zu komplettieren. 73 | 74 | ``` 75 | Buchungen in Redmine übernehmen? (j/N) 76 | j 77 | Buche 0.5h auf #34530: Einrichtung Buchungsstreber 78 | → OK 79 | Buchungen erfolgreich gespeichert 80 | ``` 81 | -------------------------------------------------------------------------------- /doc/yaml_format.md: -------------------------------------------------------------------------------- 1 | # YAML Buchungsformat 2 | 3 | ## Standard-Form 4 | 5 | ```yaml 6 | Datum: 7 | - [Zeit] [Aktivität] [Ticket-Nr.] [Beschreibung] 8 | ``` 9 | 10 | ### Beispiel 11 | 12 | ```yaml 13 | 2019-01-01: 14 | - 1.5 Orga 12345 Nachbereitung 15 | ``` 16 | 17 | ## Templates 18 | 19 | Konfiguration: 20 | 21 | ```yaml 22 | templates: 23 | BeispielDaily: 24 | activity: Daily 25 | issue: S99999 26 | text: Daily 27 | ``` 28 | 29 | Buchung: 30 | 31 | ```yaml 32 | - 0.5 BeispielDaily 33 | ``` 34 | 35 | Wird gebucht als: 36 | 37 | ```yaml 38 | 2019-01-01: 39 | - 0.5 Daily S99999 Daily 40 | ``` 41 | 42 | ## Zeitangaben 43 | 44 | Es sind Stunden-Angaben (`1.25`) sowie Zeitraeume (`Uhrzeit-Uhrzeit`) verwendbar. 45 | 46 | ```yaml 47 | 2019-01-01: 48 | - 0.5 Daily S99999 Daily 49 | - 9:00-9:15 Daily S99999 Daily 50 | ``` 51 | 52 | ## Zeitgranularitaet 53 | 54 | Zeiten werden aufgerundet. Die mindestens zu buchende Zeit kann über die 55 | Konfiguration `minimum_time` konfiguriert werden. Ist beispielsweise eine 56 | Viertelstunde konfiguriert 57 | 58 | ``` 59 | minimum_time: 0.25 60 | ``` 61 | 62 | wird 63 | 64 | ```yaml 65 | 2019-01-01: 66 | - 7:00-7:25 Daily S99999 Daily 67 | ``` 68 | 69 | gebucht als 70 | 71 | ```yaml 72 | 2019-01-01: 73 | - 0.5 Daily S99999 Daily 74 | ``` 75 | 76 | ## Aggregation 77 | 78 | ```yaml 79 | 2019-01-01: 80 | - 7:00-7:20 Daily S99999 Daily 81 | - 9:00-9:15 S99999 82 | ``` 83 | 84 | Wird gebucht als: 85 | 86 | ```yaml 87 | 2019-01-01: 88 | - 0.75 Daily S99999 Daily 89 | ``` 90 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.7.8-alpine 2 | LABEL maintainer="synyx" 3 | LABEL org.opencontainers.image.source https://github.com/synyx/buchungsstreber 4 | 5 | RUN apk update && \ 6 | apk add --no-cache \ 7 | bash \ 8 | vim \ 9 | gcc \ 10 | ncurses-dev \ 11 | libc-dev \ 12 | make \ 13 | && rm -rf /var/cache/apk/* 14 | 15 | RUN gem source --add https://nexus.synyx.de/content/repositories/gems/ && \ 16 | gem install buchungsstreber ncursesw filewatcher 17 | 18 | RUN adduser -D bs 19 | 20 | USER bs 21 | WORKDIR /home/bs 22 | 23 | CMD ["/bin/bash"] 24 | -------------------------------------------------------------------------------- /example.buchungen.yml: -------------------------------------------------------------------------------- 1 | 2019-06-17: 2 | - 0.25 BeispielDaily 3 | - 1.25 Orga S8484 Blog-Beitrag schreiben 4 | - 6.5 ES 99999 Feature implementiert 5 | # Auch die Angabe von Zeitspannen (ohne Leerzeichen) ist möglich 6 | # und wird auf volle Viertelstunden aufgerundet: 7 | - 12:00-12:35 ES 99999 Weiteres Feature implementiert 8 | 9 | 2019-06-18: 10 | - 0.5 BeispielDaily 11 | - 7 Wkly 99999 Weekly 12 | -------------------------------------------------------------------------------- /example.config.yml: -------------------------------------------------------------------------------- 1 | timesheet_file: ./buchungen.yml 2 | archive_path: ./archive 3 | redmines: 4 | - 5 | name: Synyx 6 | prefix: S 7 | server: 8 | url: https://project.synyx.de 9 | apikey: 'secret' 10 | activities: 11 | 8: [DesignView, Design, View, Dsgn] 12 | 9: [Entwicklung, E] 13 | 10: [Konzeption, Kzpt] 14 | 11: [Kombination, Komb] 15 | 12: [Organisation, Orga, Daily] 16 | 13: [Besprechung, Besp] 17 | 19: [Systemadministration, Sys, Adm] 18 | 21: [Analyse, Ana] 19 | 25: [Dokumentation, Doku] 20 | 26: [Schulung, Schu] 21 | 32: [Support, Sup] 22 | 33: [Marketing] 23 | 46: [Sales] 24 | 47: [Consulting] 25 | - 26 | name: example 27 | default: 1 28 | prefix: C 29 | server: 30 | url: https://projects.example.net 31 | apikey: 'secret' 32 | activities: 33 | 16: [AdministrationTechnisch, Adm, AT] 34 | 10: [Analyse, Ana] 35 | 26: [DailyWeekly, Daily, Dly, Wkly] 36 | 8: [Design, Dsgn] 37 | 17: [Dokumentation, Doku] 38 | 22: [EinarbeitungGeben, EinG] 39 | 28: [EinarbeitungEmpfangen, EinE] 40 | 19: [MaintenanceSupport, Sup] 41 | 11: [Organisatorisches, Orga] 42 | 24: [Planning, Pla] 43 | 29: [Retro, Ret] 44 | 23: [Review, Rev] 45 | 25: [Workshop, Ws] 46 | 66: [Monthly] 47 | 79: [Refinement, Ref] 48 | 80: [AbstimmungStakeholder, Stakeholder, Sh] 49 | 81: [EntwicklungSolo, ES, Solo, Dev, EntwicklungPair, EP, Pair] 50 | templates: 51 | BeispielDaily: 52 | activity: Daily 53 | issue: S99999 54 | text: Daily 55 | -------------------------------------------------------------------------------- /lib/buchungsstreber.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require "yaml" 4 | require "date" 5 | require "fileutils" 6 | 7 | require_relative 'buchungsstreber/version' 8 | require_relative 'buchungsstreber/validator' 9 | require_relative 'buchungsstreber/parser' 10 | require_relative 'buchungsstreber/parser/buch_timesheet' 11 | require_relative 'buchungsstreber/parser/yaml_timesheet' 12 | require_relative 'buchungsstreber/aggregator' 13 | require_relative 'buchungsstreber/generator' 14 | require_relative 'buchungsstreber/resolver' 15 | require_relative 'buchungsstreber/redmine_api' 16 | require_relative 'buchungsstreber/utils' 17 | require_relative 'buchungsstreber/redmines' 18 | require_relative 'buchungsstreber/config' 19 | 20 | module Buchungsstreber 21 | class Context 22 | attr_reader :redmines, :timesheet_parser, :timesheet_file, :config 23 | 24 | def initialize(file = nil, config_file = nil) 25 | @config = Config.load(config_file) 26 | 27 | @timesheet_file = file || File.expand_path(@config[:timesheet_file]) 28 | @timesheet_parser = TimesheetParser.new(@timesheet_file, @config[:templates], @config[:minimum_time]) 29 | @redmines = Redmines.new(@config[:redmines]) 30 | 31 | @config[:generators].each_key do |gc| 32 | require_relative "buchungsstreber/generator/#{gc}" 33 | rescue LoadError 34 | $stderr.puts "Ignoring unknown generator #{gc}" 35 | end 36 | @generator = Generator.new(@config[:generators]) 37 | @config[:generators].each_key do |gc| 38 | @generator.load!(gc) 39 | end 40 | 41 | require_relative "buchungsstreber/resolver/templates" 42 | require_relative "buchungsstreber/resolver/redmines" 43 | @config[:resolvers].each_key do |gc| 44 | require_relative "buchungsstreber/resolver/#{gc}" 45 | rescue LoadError 46 | $stderr.puts "Ignoring unknown resolver #{gc}" 47 | end 48 | @resolver = Resolver.new(@config) 49 | @config[:resolvers].each_key do |gc| 50 | @resolver.load!(gc) 51 | end 52 | end 53 | 54 | def entries(date = nil) 55 | entries = @timesheet_parser.parse.select { |x| date.nil? || date == x[:date] } 56 | 57 | result = { 58 | daily_hours: Hash.new(0), 59 | work_hours: Hash.new(@config[:hours]), 60 | valid: true, 61 | entries: [], 62 | } 63 | 64 | entries.each do |entry| 65 | errors = [] 66 | redmine = @redmines.get(entry[:redmine]) 67 | valid, err = fake_stderr do 68 | Validator.validate(entry, redmine) 69 | end 70 | errors << err unless valid 71 | result[:valid] &= valid 72 | result[:daily_hours][entry[:date]] += entry[:time] 73 | result[:work_hours][entry[:date]] = entry[:work_hours] if entry[:work_hours] 74 | 75 | title = 76 | begin 77 | redmine.get_issue(entry[:issue]) 78 | rescue StandardError => e 79 | valid = false 80 | errors << e.message 81 | nil 82 | end 83 | redmine = 84 | if (entry[:redmine] || '').empty? 85 | nil 86 | else 87 | entry[:redmine] 88 | end 89 | 90 | result[:entries] << { 91 | date: entry[:date], 92 | time: entry[:time], 93 | activity: entry[:activity], 94 | redmine: redmine, 95 | issue: entry[:issue], 96 | title: title, 97 | text: entry[:text], 98 | valid: valid, 99 | errors: errors 100 | } 101 | end 102 | 103 | result 104 | end 105 | 106 | def generate(date) 107 | @generator.generate(date) 108 | end 109 | 110 | def resolve(entry) 111 | @resolver.resolve(entry) 112 | end 113 | 114 | private 115 | 116 | def fake_stderr 117 | original_stderr = $stderr 118 | $stderr = StringIO.new 119 | res = yield 120 | [res, $stderr.string] 121 | ensure 122 | $stderr = original_stderr 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/buchungsstreber/aggregator.rb: -------------------------------------------------------------------------------- 1 | class Buchungsstreber::Aggregator 2 | def self.aggregate(entries) 3 | aggregated_entries = [] 4 | entries.each do |entry| 5 | possible_aggregations = aggregated_entries.select { |aggregated_entry| aggregatable?(aggregated_entry, entry) } 6 | if possible_aggregations.empty? 7 | aggregated_entries << entry.dup 8 | else 9 | possible_aggregations[-1][:time] += entry[:time] 10 | end 11 | end 12 | 13 | aggregated_entries 14 | end 15 | 16 | def self.aggregatable?(entry, extension) 17 | entry[:redmine] == extension[:redmine] && 18 | entry[:issue] == extension[:issue] && 19 | entry[:date] == extension[:date] && 20 | (entry[:text] == extension[:text] || !extension[:text]) && 21 | (entry[:activity] == extension[:activity] || !extension[:activity]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/buchungsstreber/cli/app.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'stringio' 3 | require 'tempfile' 4 | require 'i18n' 5 | 6 | include I18n::Gettext::Helpers # rubocop:disable Style/MixinUsage 7 | I18n.config.enforce_available_locales = false 8 | 9 | require 'buchungsstreber' 10 | require 'buchungsstreber/parser/yaml_timesheet' 11 | 12 | module Buchungsstreber 13 | module CLI 14 | class App < Thor 15 | class_option :debug, type: :boolean 16 | class_option :long, type: :boolean 17 | class_option :file, type: :string 18 | 19 | desc '', _('Buchen') 20 | def execute 21 | title = "BUCHUNGSSTREBER v#{VERSION}" 22 | puts style(title, :bold) 23 | puts '~' * title.length 24 | puts '' 25 | 26 | unless Config.find_config 27 | invoke :init 28 | invoke :config if automated? || yes?(_('Konfiguration editieren?')) 29 | end 30 | 31 | entries = Buchungsstreber::Context.new(options[:file]).entries 32 | aggregated = Aggregator.aggregate(entries[:entries]) 33 | tbl = aggregated.map do |e| 34 | status_color = { true => :blue, false => :red }[e[:valid]] 35 | err = e[:errors].map { |x| "<#{x.gsub(/:.*/m, '')}> " }.join('') 36 | [ 37 | e[:date].strftime("%a:"), 38 | style("%sh" % e[:time], :bold), 39 | '@', 40 | style((err || '') + (e[:title] || ''), status_color, 50), 41 | style(e[:text], 30) 42 | ] 43 | end 44 | print_table(tbl, indent: 2) 45 | 46 | return unless entries[:valid] 47 | 48 | min_date, max_date = entries[:daily_hours].keys.minmax 49 | puts style(_('Zu buchende Stunden (%s bis %s):') % {min_date: min_date, max_date: max_date}, :bold) 50 | tbl = entries[:daily_hours].map do |date, hours| 51 | color = Utils.classify_workhours(hours, entries[:work_hours][date]) 52 | ["#{date.strftime('%a')}:", style("#{hours}h", color)] 53 | end 54 | print_table(tbl, indent: 2) 55 | 56 | if automated? || yes?(_('Buchungen in Redmine uebernehmen? (y/N)')) 57 | invoke :buchen, [], entries: aggregated 58 | invoke :archivieren, [], entries: entries 59 | end 60 | rescue StandardError => e 61 | handle_error(e, options[:debug]) 62 | end 63 | 64 | default_task :execute 65 | 66 | desc 'show date', _('Buchungen anzeigen') 67 | def show(date) 68 | date = parse_date(date) 69 | entries = Buchungsstreber::Context.new(options[:file]).entries(date) 70 | aggregated = Aggregator.aggregate(entries[:entries]) 71 | tbl = aggregated.map do |e| 72 | status_color = {true => :blue, false => :red}[e[:valid]] 73 | err = e[:errors].map { |x| "<#{x.gsub(/:.*/m, '')}> " }.join('') 74 | [ 75 | e[:date].strftime("%a:"), 76 | style("%sh" % e[:time], :bold), 77 | '@', 78 | style((err || '') + (e[:title] || ''), status_color, 50), 79 | style(e[:text], 30) 80 | ] 81 | end 82 | print_table(tbl, indent: 2) 83 | 84 | puts style(_('Summa summarum (%s):') % {date: date}, :bold) 85 | tbl = entries[:daily_hours].map do |entrydate, hours| 86 | planned = entries[:work_hours][:planned] 87 | on_day = entries[:work_hours][entrydate] 88 | color = Utils.classify_workhours(hours, planned, on_day) 89 | ["#{entrydate.strftime('%a')}:", style("#{hours}h / #{planned}h (#{on_day}h)", color)] 90 | end 91 | print_table(tbl, indent: 2) 92 | rescue StandardError => e 93 | handle_error(e, options[:debug]) 94 | end 95 | 96 | desc 'buchen [date]', _('Buchen in Redmine') 97 | def buchen(date = nil) 98 | date = parse_date(date) 99 | context = Buchungsstreber::Context.new(options[:file]) 100 | entries = options[:entries] || Aggregator.aggregate(context.entries[:entries]) 101 | 102 | puts style(_('Buche'), :bold) 103 | entries.select { |e| date.nil? || date == e[:date] }.each do |entry| 104 | print style(_('Buche %