pltt 0.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e8327a3f012b1d7e9e6587751232ba29b60bc1a7c9ec2b368c0b8ef162c129f6
4
+ data.tar.gz: fc6b8868a2c388151f4fbfc77bf0b6b7d034faefeab45bf950b41d0cf16a5c3f
5
+ SHA512:
6
+ metadata.gz: c6e09108e751a25fb217d43c5a40c704a6409f0239e75c1735f02d489ca2846eb2c758cb3df8d95dbcb6007e83aec2a579b05b0007ee421a4c0588c96039f233
7
+ data.tar.gz: 5fe58994e0787ca17f4c3162436b22634cb9a3094c379d362ee100ae1d0ab51007948470ec4a765189b82798f232f09839656483004b8c3b2cdb0d7f533d47c8
@@ -0,0 +1 @@
1
+ vendor
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,106 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.5
3
+ Style/GuardClause:
4
+ MinBodyLength: 1
5
+ Enabled: false
6
+ Style/AndOr:
7
+ EnforcedStyle: conditionals
8
+ Style/PercentLiteralDelimiters:
9
+ PreferredDelimiters: {}
10
+ Style/Alias:
11
+ Enabled: false
12
+ Style/Documentation:
13
+ Enabled: false
14
+ Style/PercentLiteralDelimiters:
15
+ Enabled: false
16
+ Lint/UnusedMethodArgument:
17
+ Enabled: false
18
+ Metrics/ModuleLength:
19
+ Enabled: false
20
+ Style/StringLiterals:
21
+ Enabled: false
22
+ Style/IfUnlessModifier:
23
+ Enabled: false
24
+ Metrics/MethodLength:
25
+ Enabled: false
26
+ Metrics/LineLength:
27
+ Max: 150
28
+ Metrics/AbcSize:
29
+ Enabled: false
30
+ Metrics/ClassLength:
31
+ Enabled: false
32
+ Layout/DotPosition:
33
+ Enabled: false
34
+ Style/BlockDelimiters:
35
+ Enabled: false
36
+ Style/AndOr:
37
+ Enabled: false
38
+ Layout/AlignParameters:
39
+ EnforcedStyle: with_fixed_indentation
40
+ Rails/ActionFilter:
41
+ Enabled: false
42
+ Layout/MultilineMethodCallIndentation:
43
+ EnforcedStyle: indented
44
+ Style/FrozenStringLiteralComment:
45
+ Enabled: false
46
+ # class Admin::PageController
47
+ Style/ClassAndModuleChildren:
48
+ Enabled: false
49
+
50
+ # asddsa == 0 -> asddsa.zero?
51
+ Style/NumericPredicate:
52
+ Enabled: false
53
+ Performance/StringReplacement:
54
+ Enabled: false
55
+ # ->() {} -- lambda {|a| }
56
+ Style/Lambda:
57
+ EnforcedStyle: 'literal'
58
+ # if bla.nil? -- if bla == nil
59
+ Style/NilComparison:
60
+ Enabled: false
61
+ # Ruby 3
62
+ Style/FrozenStringLiteralComment:
63
+ Enabled: false
64
+ # Rails 5 -> im Controller/Request Test get :foo, params: {}
65
+ Rails/HttpPositionalArguments:
66
+ Enabled: false
67
+ # def bla; end
68
+ Style/EmptyMethod:
69
+ Enabled: false
70
+ # [ "700", "200" ] -> %w[w w]
71
+ Style/WordArray:
72
+ Enabled: false
73
+ # RSpec nervt
74
+ Metrics/BlockLength:
75
+ ExcludedMethods: ['task', 'namespace', 'get', 'post', 'describe', 'context', 'specify', 'it', 'route_param', 'draw', 'register', 'register_page', 'controller', 'included', 'params', 'resource', 'with_options', 'use_cassette', 'configure']
76
+
77
+ Bundler/OrderedGems:
78
+ Enabled: false
79
+ # update_all, update_column
80
+ # set_ und get_ verbote
81
+ Naming/AccessorMethodName:
82
+ Enabled: false
83
+ Style/TrailingCommaInArrayLiteral:
84
+ Enabled: false
85
+ Style/TrailingCommaInHashLiteral:
86
+ Enabled: false
87
+
88
+ Style/SymbolArray:
89
+ Enabled: false
90
+
91
+ Style/AsciiComments:
92
+ Enabled: false
93
+ Style/FormatString:
94
+ Enabled: false
95
+ Style/FormatStringToken:
96
+ Enabled: false
97
+ Metrics/PerceivedComplexity:
98
+ Enabled: false
99
+ Style/DoubleNegation:
100
+ Enabled: false
101
+ Style/ZeroLengthPredicate:
102
+ Enabled: false
103
+ Style/SafeNavigation:
104
+ Enabled: false
105
+ Lint/MissingCopEnableDirective:
106
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in pltt.gemspec
6
+ gemspec
@@ -0,0 +1,66 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pltt (0.1.0.beta)
5
+ gitlab (~> 4.4.0)
6
+ hashids
7
+ oj
8
+ terminal-table (>= 1.8.0)
9
+ thor
10
+ tty-prompt
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ coderay (1.1.2)
16
+ equatable (0.5.0)
17
+ gitlab (4.4.0)
18
+ httparty (>= 0.14.0)
19
+ terminal-table (>= 1.5.1)
20
+ hashids (1.0.4)
21
+ hitimes (1.2.6)
22
+ httparty (0.16.2)
23
+ multi_xml (>= 0.5.2)
24
+ method_source (0.9.0)
25
+ multi_xml (0.6.0)
26
+ necromancer (0.4.0)
27
+ oj (3.6.2)
28
+ pastel (0.7.2)
29
+ equatable (~> 0.5.0)
30
+ tty-color (~> 0.4.0)
31
+ pry (0.11.3)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.9.0)
34
+ rake (10.5.0)
35
+ terminal-table (1.8.0)
36
+ unicode-display_width (~> 1.1, >= 1.1.1)
37
+ thor (0.20.0)
38
+ timers (4.1.2)
39
+ hitimes
40
+ tty-color (0.4.2)
41
+ tty-cursor (0.5.0)
42
+ tty-prompt (0.16.1)
43
+ necromancer (~> 0.4.0)
44
+ pastel (~> 0.7.0)
45
+ timers (~> 4.0)
46
+ tty-cursor (~> 0.5.0)
47
+ tty-reader (~> 0.3.0)
48
+ tty-reader (0.3.0)
49
+ tty-cursor (~> 0.5.0)
50
+ tty-screen (~> 0.6.4)
51
+ wisper (~> 2.0.0)
52
+ tty-screen (0.6.4)
53
+ unicode-display_width (1.4.0)
54
+ wisper (2.0.0)
55
+
56
+ PLATFORMS
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ bundler (~> 1.16)
61
+ pltt!
62
+ pry
63
+ rake (~> 10.0)
64
+
65
+ BUNDLED WITH
66
+ 1.16.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Stefan Wienert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,91 @@
1
+ # Pltt
2
+
3
+ Pltt is a Gitlab Time Tracker for the command line. It mimicks the interface of [kriskbx/gitlab-time-tracker]( https://github.com/kriskbx/gitlab-time-tracker ) and is compatible to the config and frame database. Thus, it works as a drop-in-replacement.
4
+
5
+ Goals/Pros:
6
+
7
+ * Fast interface, even though it's ruby, the boot time is faster right now
8
+ * More friendly interface, error persistent, e.g. forgotten stoppings, issue creation with question-response cycles
9
+ * More robust interface with Gitlab:
10
+ * Not possible to book on non-existend or closed issues
11
+ * Time Entries are booked on stop by default, so no time shifting
12
+ * pltt status shows issue body, milestone etc. too
13
+
14
+ ## Installation
15
+
16
+ Install it yourself as:
17
+
18
+ $ gem install pltt
19
+
20
+ ## Usage
21
+
22
+ ### Starting Time Tracking
23
+
24
+ ```bash
25
+ # start without issue id -> shows a selection menu of 30 most recent issues in project
26
+ pltt start
27
+
28
+ # start with issue id -> validates issue exists and is not closed
29
+ pltt start 123
30
+
31
+ # create new issue with interactive question for issue creation
32
+ # issue will be created instantly, not on sync
33
+ # You can enter issue title, select labels, and description
34
+ pltt create
35
+
36
+ # Starts time tracking with
37
+ pltt resume
38
+ ```
39
+
40
+ ### Stopping, Synching and Maintenance
41
+
42
+ ```bash
43
+ # stops time tracking & syncs to gitlab
44
+ # 2 interactive questions can easly fix missing started entries
45
+ # can adjust start time and duration (minutes)
46
+ pltt stop
47
+
48
+ # Edit current entry in EDITOR
49
+ pltt edit
50
+ pltt edit 19ad20
51
+
52
+ # Cancel = delete current running entry
53
+ pltt cancel
54
+
55
+ # normally not needed, as sync is made after stop, only in case of error
56
+ pltt sync
57
+ ```
58
+
59
+ ### Reporting / General
60
+
61
+ ```bash
62
+ # show all issues in project as table
63
+ pltt list
64
+ pltt list --my
65
+ pltt list --label Bug
66
+ ```
67
+
68
+ ## Development
69
+
70
+ Missing before release:
71
+
72
+ * [x] gtt start with no arguments shows the list of open issues to select one from
73
+ * [x] gtt create can optionally create a MR + branch
74
+ * [x] gtt stop / sync note creation
75
+ * [ ] First-Start Guide, Generator which creates .gtt.yml in a project, gets the project url from .git/config
76
+ * [ ] Timezone
77
+ * [ ] Frame Cleanup, everything older than X month can delete
78
+
79
+ Missing, but not planned soon, as not needed by us:
80
+
81
+ * [ ] Logging/reporting of all bookings
82
+ * [ ] No Booking on Merge requests implemented
83
+ * [ ] No delete implemented, as cancel is enough. deleting frames is also not very useful, when they are already synced to Gitlab
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pludoni/pltt.
88
+
89
+ ## License
90
+
91
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pltt"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ dir = File.join(File.dirname(File.realdirpath(__FILE__)), '..')
3
+
4
+ if RUBY_VERSION < '1.9.2'
5
+ $stderr.puts 'your ruby is too old!'
6
+ exit 1
7
+ end
8
+
9
+ $LOAD_PATH.unshift File.join(dir, 'lib')
10
+
11
+ ENV['BUNDLE_GEMFILE'] ||= File.join(dir, 'Gemfile')
12
+
13
+ require 'pltt'
14
+ require 'bundler/setup'
15
+ require 'pltt/runner'
@@ -0,0 +1,6 @@
1
+ require "pltt/version"
2
+
3
+ module Pltt
4
+ module Actions
5
+ end
6
+ end
@@ -0,0 +1,105 @@
1
+ require 'yaml'
2
+ require 'oj'
3
+ require_relative '../entry'
4
+ require_relative '../colorize'
5
+
6
+ class Pltt::Actions::Base
7
+ def self.run(*args)
8
+ new.run(*args)
9
+ end
10
+
11
+ def config
12
+ self.class.config
13
+ end
14
+
15
+ def self.config
16
+ return @config if @config
17
+ master = File.expand_path('~/.gtt/config.yml')
18
+ unless File.exist?(master)
19
+ raise ".gtt/config.yml existiert nicht!"
20
+ end
21
+ config = YAML.load_file(master)
22
+ locale = File.join(Dir.pwd, '.gtt.yml')
23
+ if File.exist?(locale)
24
+ config = config.merge(YAML.load_file(locale))
25
+ end
26
+ config['frame_dir'] ||= '~/.gtt/frames/'
27
+ config['gitlab_url'] ||= config['url'].sub(%r{/api/v4/?}, '')
28
+ @config = config
29
+ end
30
+
31
+ def current_entry
32
+ max = Time.now - 3600 * 24 * 7
33
+ recent_entries = all_frames.select { |i| File.mtime(i) > max }
34
+ recent_entries.each do |entry|
35
+ e = load_entry(entry)
36
+ return e if e.current?
37
+ end
38
+ nil
39
+ end
40
+
41
+ def load_entry(path_to_frame)
42
+ File.read(path_to_frame).yield_self { |i| Oj.load(i) }.yield_self(&Pltt::Entry.method(:new))
43
+ end
44
+
45
+ def all_frames
46
+ Dir[File.join(File.expand_path(config['frame_dir']), '*.json')]
47
+ end
48
+
49
+ def exit_if_running!
50
+ if current_entry
51
+ puts "Already started!".red
52
+ puts current_entry.status
53
+ exit 1
54
+ end
55
+ end
56
+
57
+ def exit_if_not_running!
58
+ unless current_entry
59
+ puts "Not started!".red
60
+ exit 1
61
+ end
62
+ end
63
+
64
+ def gitlab_api
65
+ @gitlab_api ||=
66
+ begin
67
+ require_relative '../gitlab_wrapper'
68
+ Pltt::GitlabWrapper.new(config['url'], config['token'], config['project'])
69
+ end
70
+ end
71
+
72
+ def sync_all_unsaved_entries
73
+ gitlab_api
74
+ max = Time.now - 3600 * 24 * 7
75
+ recent_entries = all_frames.select { |i| File.mtime(i) > max }
76
+ recent_entries.each do |e|
77
+ entry = load_entry(e)
78
+ next if entry.synced? || entry.stop.nil?
79
+ sync!(entry)
80
+ end
81
+ end
82
+
83
+ def sync!(entry)
84
+ c = entry
85
+ minutes = c.duration_seconds.round / 60
86
+ require 'pry'
87
+
88
+ min_part = minutes % 60
89
+ hour_part = minutes / 60
90
+
91
+ gitlab_time_format =
92
+ case [hour_part > 0, min_part > 0]
93
+ when [true, true] then "#{hour_part}h#{min_part}m"
94
+ when [false, true] then "#{min_part}m"
95
+ when [true, false] then "#{hour_part}m"
96
+ else return
97
+ end
98
+ puts "Synching: #{gitlab_time_format.inspect} on #{c.project}##{c.iid}"
99
+ gitlab_api.add_time_spent_on_issue(c.project, c.iid, gitlab_time_format)
100
+
101
+ note_id = Gitlab.issue_notes(entry.project, entry.iid).auto_paginate.max_by { |i| i.created_at }.id
102
+ c.add_note(note_id, c.duration_seconds.round)
103
+ c.persist!
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ require_relative './base'
2
+ require 'tty-prompt'
3
+ class Pltt::Actions::Create < Pltt::Actions::Base
4
+ def run
5
+ puts "New issue in project #{config['project'].green}:"
6
+ prompt = TTY::Prompt.new
7
+ title = prompt.ask('Enter issue title:') do |q|
8
+ q.required true
9
+ end
10
+ description = prompt.multiline("Description").join("\n")
11
+ labels = gitlab_api.labels.map(&:name)
12
+ selected_labels = if labels.length > 0
13
+ prompt.multi_select("Select labels", labels, per_page: 20)
14
+ else
15
+ []
16
+ end
17
+ issue = gitlab_api.create_issue(title, description, selected_labels)
18
+ _entry = Pltt::Entry.create_new_for_gitlab_issue(config['project'], issue)
19
+
20
+ branch_name = "#{issue.iid}-#{issue.title.downcase.gsub(/[\W]+/, '-')}"
21
+ if prompt.yes?("Automatically create a branch #{branch_name} and check out locally?")
22
+ gitlab_api.create_branch_and_merge_request(branch_name, issue)
23
+ system 'git fetch'
24
+ system "git checkout #{branch_name}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ require_relative './base'
2
+ class Pltt::Actions::Edit < Pltt::Actions::Base
3
+ def run(id)
4
+ if id
5
+ raise NotImplementedError
6
+ else
7
+ exit_if_not_running!
8
+ c = current_entry
9
+ c.frame_path
10
+
11
+ exec ENV['EDITOR'] || 'vim', c.frame_path
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ require_relative './base'
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+ class Pltt::Actions::List < Pltt::Actions::Base
4
+ def run(my: false, label: nil)
5
+ require 'terminal-table'
6
+ require 'pry'
7
+
8
+ table = Terminal::Table.new
9
+ gitlab_api.issues(scope: my ? 'assigned-to-me' : 'all', label: label).each do |issue|
10
+ table.add_row [
11
+ issue.iid.to_s.magenta,
12
+ issue.title.green + "\n#{issue.web_url.dark_gray}",
13
+ issue.state.gray,
14
+ issue.milestone&.title,
15
+ issue.labels.map { |i| "[~#{i}]" }.join
16
+ ]
17
+ end
18
+ puts table
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require_relative './base'
2
+ class Pltt::Actions::Resume < Pltt::Actions::Base
3
+ def run
4
+ require 'pry'
5
+ exit_if_running!
6
+
7
+ last_one = all_frames.sort_by { |i| File.mtime(i) }.reverse.lazy.
8
+ map { |i| load_entry(i) }.
9
+ select { |i| i.project == config['project'] }.
10
+ take(5).max_by { |i| i.stop }
11
+
12
+ unless last_one
13
+ puts "No previous entry found for #{config['project']}!".red
14
+ exit 1
15
+ end
16
+
17
+ new_entry = Pltt::Entry.from_resume(last_one)
18
+ new_entry.persist!
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ require_relative './base'
2
+ class Pltt::Actions::Start < Pltt::Actions::Base
3
+ def run(iid)
4
+ exit_if_running!
5
+
6
+ issue = nil
7
+ if iid
8
+ issue = gitlab_api.issue(iid.to_i)
9
+ puts "Issue: #{issue.title.green}"
10
+ else
11
+ require 'tty-prompt'
12
+ prompt = TTY::Prompt.new
13
+ recent = gitlab_api.issues.take(30)
14
+ issue = prompt.select("Select issue to start on", per_page: 20) do |menu|
15
+ recent.each do |this_issue|
16
+ menu.choice "#{this_issue.iid.to_s.magenta} #{this_issue.title}", this_issue
17
+ end
18
+ end
19
+ end
20
+ start_by_issue(issue)
21
+ rescue StandardError => e
22
+ puts "Issue #{issue.iid} not found in project #{config['project']}".red
23
+ puts e.inspect
24
+ exit 1
25
+ end
26
+
27
+ def start_by_issue(issue)
28
+ if issue.closed?
29
+ puts "This issue is closed"
30
+ exit 1
31
+ end
32
+ Pltt::Entry.create_new_for_gitlab_issue(config['project'], issue)
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ require_relative './base'
2
+ class Pltt::Actions::Status < Pltt::Actions::Base
3
+ def run
4
+ exit_if_not_running!
5
+ puts current_entry.status
6
+
7
+ if (iid = current_entry.resource['id'])
8
+ issue = gitlab_api.issue(iid)
9
+ puts "-------------------------------------------".black
10
+ puts <<~DOC
11
+ #{issue.title.green} (##{iid})
12
+ #{issue.time_stats.human_total_time_spent&.dark_gray} #{issue.assignees.map { |i| "@#{i['username']}".blue }.join(' ')}
13
+ #{issue.labels.map { |i| "[~#{i}]".red }.join(' ')} #{issue.milestone&.title&.magenta}
14
+
15
+ #{issue.description}
16
+ DOC
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ require_relative './base'
2
+ require 'tty-prompt'
3
+ # rubocop:disable Style/NegatedIf
4
+ class Pltt::Actions::Stop < Pltt::Actions::Base
5
+ def run
6
+ exit_if_not_running!
7
+ c = current_entry
8
+
9
+ minutes = (c.duration_seconds / 60).round
10
+ puts "Currently running for #{minutes} min"
11
+ prompt = TTY::Prompt.new
12
+ too_large = minutes > 480
13
+ if !prompt.no?("Was started on #{c.start.localtime.to_s.green}, fix beginning?")
14
+ time = prompt.ask("Enter beginning *time* only (HH:MM)") do |q|
15
+ q.required true
16
+ q.validate(/\A\d+:\d+\Z/)
17
+ q.convert :string
18
+ end
19
+ h, m = time.split(':').map(&:to_i)
20
+ c.start = Time.new(c.start.year, c.start.month, c.start.day, h, m)
21
+ end
22
+ minutes = (c.duration_seconds / 60).round
23
+ puts "Currently running for #{minutes} min"
24
+ if too_large || !prompt.no?("Fix duration before saving? (#{minutes}min)")
25
+ puts "Error: To many minutes, must be not greater than 8h (480min)" if too_large
26
+ new_minutes = prompt.ask("New Minutes, beginning from #{c.start.localtime}") do |q|
27
+ q.required true
28
+ q.convert :int
29
+ end
30
+ stop = (c.start + new_minutes * 60).utc
31
+ else
32
+ stop = Time.now.utc
33
+ end
34
+ c.stop = stop
35
+
36
+ c.persist!
37
+
38
+ sync_all_unsaved_entries
39
+ rescue StandardError => e
40
+ puts "Issue #{c.id} not found in project #{config['project']}".red
41
+ puts e.inspect
42
+ exit 1
43
+ end
44
+ end
@@ -0,0 +1,93 @@
1
+ class String
2
+ def dark_gray
3
+ "\e[90m#{self}\e[0m"
4
+ end
5
+
6
+ def black
7
+ "\e[30m#{self}\e[0m"
8
+ end
9
+
10
+ def red
11
+ "\e[31m#{self}\e[0m"
12
+ end
13
+
14
+ def green
15
+ "\e[32m#{self}\e[0m"
16
+ end
17
+
18
+ def brown
19
+ "\e[33m#{self}\e[0m"
20
+ end
21
+
22
+ def blue
23
+ "\e[34m#{self}\e[0m"
24
+ end
25
+
26
+ def magenta
27
+ "\e[35m#{self}\e[0m"
28
+ end
29
+
30
+ def cyan
31
+ "\e[36m#{self}\e[0m"
32
+ end
33
+
34
+ def gray
35
+ "\e[37m#{self}\e[0m"
36
+ end
37
+
38
+ def gray39
39
+ "\e[39m#{self}\e[0m"
40
+ end
41
+
42
+ def bg_black
43
+ "\e[40m#{self}\e[0m"
44
+ end
45
+
46
+ def bg_red
47
+ "\e[41m#{self}\e[0m"
48
+ end
49
+
50
+ def bg_green
51
+ "\e[42m#{self}\e[0m"
52
+ end
53
+
54
+ def bg_brown
55
+ "\e[43m#{self}\e[0m"
56
+ end
57
+
58
+ def bg_blue
59
+ "\e[44m#{self}\e[0m"
60
+ end
61
+
62
+ def bg_magenta
63
+ "\e[45m#{self}\e[0m"
64
+ end
65
+
66
+ def bg_cyan
67
+ "\e[46m#{self}\e[0m"
68
+ end
69
+
70
+ def bg_gray
71
+ "\e[47m#{self}\e[0m"
72
+ end
73
+
74
+ def bold
75
+ "\e[1m#{self}\e[22m"
76
+ end
77
+
78
+ def italic
79
+ "\e[3m#{self}\e[23m"
80
+ end
81
+
82
+ def underline
83
+ "\e[4m#{self}\e[24m"
84
+ end
85
+
86
+ def blink
87
+ "\e[5m#{self}\e[25m"
88
+ end
89
+
90
+ def reverse_color
91
+ "\e[7m#{self}\e[27m"
92
+ end
93
+ end
@@ -0,0 +1,99 @@
1
+ class Pltt::Entry
2
+ attr_reader :id, :project, :start, :stop, :resource
3
+ attr_writer :start, :stop
4
+
5
+ def self.from_resume(other_entry)
6
+ new('project' => other_entry.project, 'resource' => other_entry.resource, 'start' => Time.now.utc.iso8601)
7
+ end
8
+
9
+ def self.create_new_for_gitlab_issue(project, issue)
10
+ entry = Pltt::Entry.new('project' => project, 'resource' => { 'id' => issue.iid, 'type' => 'issue' }, 'start' => Time.now.utc.iso8601)
11
+ entry.persist!
12
+ entry
13
+ end
14
+
15
+ def initialize(json)
16
+ @id = json['id']
17
+ @project = json['project']
18
+ @resource = json['resource']
19
+ @notes = json['notes'] || []
20
+ @start = Time.iso8601(json['start']) if json['start']
21
+ @stop = if json['stop']
22
+ Time.iso8601(json['stop'])
23
+ else
24
+ false
25
+ end
26
+ @_json = json
27
+ end
28
+
29
+ def current?
30
+ !@stop
31
+ end
32
+
33
+ def status
34
+ duration = duration_time.strftime("%H:%M:%S")
35
+ <<~DOC
36
+ #{@project}##{iid}, #{duration.green} (id: #{@id})
37
+ #{url.gray}
38
+ DOC
39
+ end
40
+
41
+ def duration_time
42
+ Time.at(duration_seconds).utc
43
+ end
44
+
45
+ def duration_seconds
46
+ (@stop || Time.now.utc) - @start.utc
47
+ end
48
+
49
+ def url
50
+ "#{Pltt::Actions::Base.config['gitlab_url']}/#{@project}/issues/#{iid}"
51
+ end
52
+
53
+ def frame_path
54
+ @frame_path ||= File.join(File.expand_path(Pltt::Actions::Base.config['frame_dir']), @id + ".json")
55
+ end
56
+
57
+ def cancel!
58
+ if File.exist?(frame_path)
59
+ puts "Entry deleted"
60
+ File.unlink(frame_path)
61
+ else
62
+ puts "Entry already deleted!"
63
+ exit 1
64
+ end
65
+ end
66
+
67
+ def persist!
68
+ require 'oj'
69
+ Oj.mimic_JSON
70
+ unless @id
71
+ require 'hashids'
72
+ @id = Hashids.new('', 9).encode(@start.to_i)
73
+ end
74
+ output = JSON.pretty_generate(id: @id,
75
+ project: @project,
76
+ resource: @resource,
77
+ notes: @notes,
78
+ start: @start && @start.iso8601,
79
+ stop: @stop && @stop.iso8601)
80
+ File.write(frame_path, output)
81
+ unless @stop
82
+ puts "Started:"
83
+ end
84
+ puts status
85
+ end
86
+
87
+ def add_note(id, time)
88
+ @notes ||= []
89
+ @notes << { id: id, time: time }
90
+ end
91
+
92
+ def iid
93
+ @resource['id']
94
+ end
95
+
96
+ def synced?
97
+ @notes && @notes.length > 0
98
+ end
99
+ end
@@ -0,0 +1,40 @@
1
+ class Pltt::GitlabWrapper
2
+ def initialize(url, token, project)
3
+ require 'gitlab'
4
+ @project = project
5
+
6
+ Gitlab.endpoint = url
7
+ Gitlab.private_token = token
8
+ end
9
+
10
+ def issues(scope: 'assigned-to-me', order_by: 'updated_at', state: 'opened', label: nil)
11
+ Gitlab.issues(@project, state: state, order_by: order_by, scope: scope, labels: label ? label : nil).auto_paginate
12
+ end
13
+
14
+ def issue(id)
15
+ Gitlab.issue(@project, id)
16
+ end
17
+
18
+ def create_issue(title, description, labels)
19
+ user_id = Gitlab.user.id
20
+ Gitlab.create_issue(@project, title, description: description, labels: labels.join(','), assignee_ids: [user_id])
21
+ end
22
+
23
+ def labels
24
+ Gitlab.labels(@project)
25
+ end
26
+
27
+ def create_branch_and_merge_request(branch_name, issue)
28
+ Gitlab.create_branch(@project, branch_name, 'master')
29
+ Gitlab.create_merge_request(@project, "Resolve #{issue.title}",
30
+ source_branch: branch_name,
31
+ target_branch: 'master',
32
+ description: "Closes ##{issue.iid}",
33
+ remove_source_branch: true)
34
+
35
+ end
36
+
37
+ def add_time_spent_on_issue(project, issue_id, duration)
38
+ Gitlab.add_time_spent_on_issue(project, issue_id, duration)
39
+ end
40
+ end
@@ -0,0 +1,66 @@
1
+ require 'thor'
2
+
3
+ class Pltt::Runner < Thor
4
+ desc "resume", "resume last issue in current project"
5
+ def resume
6
+ require_relative './actions/resume'
7
+ Pltt::Actions::Resume.run
8
+ end
9
+
10
+ desc "create", "create new issue in current project"
11
+ def create
12
+ require_relative './actions/create'
13
+ Pltt::Actions::Create.run
14
+ end
15
+
16
+ desc "start [ID]", "start tracking for issue #ID"
17
+ def start(id = nil)
18
+ require_relative './actions/start'
19
+ Pltt::Actions::Start.run(id)
20
+ end
21
+
22
+ desc "stop", "Stop"
23
+ def stop
24
+ require_relative './actions/stop'
25
+ Pltt::Actions::Stop.run
26
+ end
27
+
28
+ desc "list", "List all project issues"
29
+ option :my, type: :boolean
30
+ option :label, type: :string
31
+ def list
32
+ require_relative './actions/list'
33
+ Pltt::Actions::List.run(my: options[:my], label: options[:label])
34
+ end
35
+
36
+ desc "status", "Status"
37
+ def status
38
+ require_relative './actions/status'
39
+ Pltt::Actions::Status.run
40
+ end
41
+
42
+ desc "edit [ID = CURRENT]", "start tracking for issue #ID"
43
+ def edit(id = nil)
44
+ require_relative './actions/edit'
45
+ Pltt::Actions::Edit.run(id)
46
+ end
47
+
48
+ desc "cancel", "cancel current entry"
49
+ def cancel
50
+ require_relative './actions/base'
51
+ if (c = Pltt::Actions::Base.new.current_entry)
52
+ c.cancel!
53
+ else
54
+ puts "No running entry to cancel"
55
+ exit 1
56
+ end
57
+ end
58
+
59
+ desc 'sync', 'sync all unsaved entries'
60
+ def sync
61
+ require_relative './actions/base'
62
+ Pltt::Actions::Base.new.sync_all_unsaved_entries
63
+ end
64
+ end
65
+
66
+ Pltt::Runner.start(ARGV)
@@ -0,0 +1,3 @@
1
+ module Pltt
2
+ VERSION = "0.1.0.beta"
3
+ end
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "pltt/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pltt"
7
+ spec.version = Pltt::VERSION
8
+ spec.authors = ["Stefan Wienert"]
9
+ spec.email = ["info@stefanwienert.de"]
10
+
11
+ spec.summary = %{}
12
+ spec.description = %{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency 'gitlab', '~> 4.4.0'
24
+ spec.add_dependency 'hashids'
25
+ spec.add_dependency 'oj'
26
+ spec.add_dependency 'terminal-table', '>= 1.8.0'
27
+ spec.add_dependency 'thor'
28
+ spec.add_dependency 'tty-prompt'
29
+ spec.add_development_dependency "bundler", "~> 1.16"
30
+ spec.add_development_dependency "pry"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pltt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.beta
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Wienert
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gitlab
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashids
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: oj
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.8.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-prompt
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.16'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.16'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '10.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '10.0'
139
+ description: ''
140
+ email:
141
+ - info@stefanwienert.de
142
+ executables:
143
+ - pltt
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".agignore"
148
+ - ".gitignore"
149
+ - ".rubocop.yml"
150
+ - Gemfile
151
+ - Gemfile.lock
152
+ - LICENSE.txt
153
+ - README.md
154
+ - Rakefile
155
+ - bin/console
156
+ - bin/setup
157
+ - exe/pltt
158
+ - lib/pltt.rb
159
+ - lib/pltt/actions/base.rb
160
+ - lib/pltt/actions/create.rb
161
+ - lib/pltt/actions/edit.rb
162
+ - lib/pltt/actions/list.rb
163
+ - lib/pltt/actions/resume.rb
164
+ - lib/pltt/actions/start.rb
165
+ - lib/pltt/actions/status.rb
166
+ - lib/pltt/actions/stop.rb
167
+ - lib/pltt/colorize.rb
168
+ - lib/pltt/entry.rb
169
+ - lib/pltt/gitlab_wrapper.rb
170
+ - lib/pltt/runner.rb
171
+ - lib/pltt/version.rb
172
+ - pltt.gemspec
173
+ homepage: ''
174
+ licenses:
175
+ - MIT
176
+ metadata: {}
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">"
189
+ - !ruby/object:Gem::Version
190
+ version: 1.3.1
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 2.7.7
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: ''
197
+ test_files: []