jirawatch 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ # Jirawatch
2
+ Jirawatch is a simple CLI tool that allows you to track the time you spend on a particular Jira issue without having to access your crowded backlog/kanban.
3
+ Tracking time is as simple as typing:
4
+ ```
5
+ jirawatch track ISSUE-873
6
+ ```
7
+ After that, jirawatch will start tracking your time and, once you're done, you can press `Ctrl-c` to stop the time tracking and allow jirawatch to save it into the Jira worklogs of the related issue.
8
+ If you need to stop tracking time, just press `Ctrl-p` and then press it again to resume.
9
+
10
+ ## Installation
11
+
12
+ ### Gem
13
+
14
+ Simply run
15
+ ```
16
+ gem install jirawatch
17
+ ```
18
+ And you're good to go!
19
+
20
+ ### With Docker/Podman
21
+
22
+ You can execute `jirawatch` without installing it on your machine! Jirawatch is available via Docker/Podman too.
23
+ To run it just type:
24
+ ```
25
+ docker run apontini/jirawatch
26
+ ```
27
+ and you're good to go!
28
+
29
+ If you feel like doing everything by yourself it can be built from the repository with:
30
+ ```
31
+ docker build -t <your username>/jirawatch
32
+ docker run <your username>/jirawatch # enjoy the magic
33
+ ```
34
+
35
+ Keep in mind that you need to setup persistence in order to store Jirawatch's configuration files. You can put in your `.bashrc` or `.zshrc` the following alias:
36
+ ```
37
+ alias jirawatch="docker run --rm -it -v${HOME}/.jirawatch:/root/.jirawatch -v/etc/localtime:/etc/localtime:ro apontini/jirawatch"
38
+ ```
39
+ This will ensure persistence and will not modify your machine with additional packages ;)
40
+
41
+ ### Manually
42
+
43
+ There's no install script right now unfortunately (it will be coming though!).
44
+ For the time being, you need to install Ruby (2.7.0 was used do develop this gem but it should work from 2.1.0 onwards) using [rbenv](https://github.com/rbenv/rbenv).
45
+
46
+ As a side note, I highly discourage installing Ruby without rbenv and installing rbenv from a package manager (reason is that rbenv packages are severely outdated most of the time), unless you know what you're doing of course!
47
+
48
+ After that, clone this repository and build this gem using:
49
+ ```
50
+ gem build jirawatch.gemspec
51
+ ```
52
+ and install it with:
53
+ ```
54
+ gem install jirawatch-<gem-version>.gem
55
+ ```
56
+
57
+ You should now be good to go!
58
+
59
+ ## How to use
60
+ As I stated before, this is not a complex tool! There are a few commands implemented as of right now:
61
+ ```
62
+ jirawatch version # Prints the current jirawatch version
63
+ jirawatch login # Allows to log you in to Jira then saves your credentials if the operation succeeded
64
+ jirawatch projects # Lists all Jira projects
65
+ jirawatch issues [project-key] # Lists every issue related to a project
66
+ jirawatch track [issue-key] # Starts tracking time for an issue
67
+ ```
68
+
69
+ You need to register your Jira credentials first, before you can start tracking. Be sure to have a valid API Token for your account (which you can generate at https://id.atlassian.com/manage/api-tokens).
70
+ Then use the `login` command and follow the prompt instructions.
71
+ If everything is successful, you're good to go!
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env -S ruby -W0
2
+ require 'bundler/setup'
3
+ require 'dry/cli'
4
+ Dir["#{__dir__}/../lib/jirawatch/**/*.rb"].sort.map { |file| File.expand_path(file) }.each do |file|
5
+ require file
6
+ end
7
+ require 'jirawatch'
8
+
9
+ include Jirawatch::Utils::Messages
10
+
11
+ begin
12
+ Dry::CLI.new(Jirawatch).call
13
+ rescue Jirawatch::Errors::CommandFailed => e
14
+ puts e.message
15
+ exit -1
16
+ rescue JIRA::HTTPError => e
17
+ puts e.response.body
18
+ exit -1
19
+ rescue StandardError => e
20
+ puts e.backtrace
21
+ puts e.message
22
+ exit -1
23
+ end
@@ -0,0 +1,19 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'jirawatch/info'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'jirawatch'
7
+ s.version = Jirawatch::Info::VERSION
8
+ s.date = '2020-02-27'
9
+ s.summary = "Jira time tracker"
10
+ s.description = "A simple way to track jira issues time"
11
+ s.authors = ["apontini"]
12
+ s.email = 'alberto.pontini@gmail.com'
13
+ s.files = `git ls-files`.split($/)
14
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+
16
+ s.add_dependency "dry-cli"
17
+ s.add_dependency "jira-ruby"
18
+ s.add_dependency "tty-editor"
19
+ end
@@ -0,0 +1,35 @@
1
+ require 'fileutils'
2
+
3
+ module Jirawatch
4
+ extend Dry::CLI::Registry
5
+ include Jirawatch::Jira::Provisioning
6
+
7
+ attr_accessor :configuration
8
+ attr_accessor :strings
9
+
10
+ class << self
11
+ def configuration
12
+ @configuration || Jirawatch::Config.new
13
+ end
14
+
15
+ def configure
16
+ @configuration ||= Jirawatch::Config.new
17
+ yield @configuration
18
+ end
19
+
20
+ def strings
21
+ @strings || Jirawatch::Strings.new
22
+ end
23
+ end
24
+
25
+ unless Dir.exist? configuration.config_path
26
+ puts "Creating jirawatch config directory at #{configuration.config_path}"
27
+ FileUtils.mkdir_p configuration.config_path
28
+ end
29
+ end
30
+
31
+ Jirawatch.register "version", Jirawatch::CLI::Version
32
+ Jirawatch.register "track", Jirawatch::CLI::Track
33
+ Jirawatch.register "login", Jirawatch::CLI::Login
34
+ Jirawatch.register "projects", Jirawatch::CLI::Projects
35
+ Jirawatch.register "issues", Jirawatch::CLI::Issues
@@ -0,0 +1,31 @@
1
+ require 'jirawatch/jira/provisioning'
2
+
3
+ module Jirawatch
4
+ module CLI
5
+ module AuthenticatedCommand
6
+ include Jirawatch::Jira::Provisioning
7
+
8
+ def self.included(base)
9
+
10
+ def base.method_added(name)
11
+ if name.eql? :call and not method_defined? :alias_call
12
+ alias_method :alias_call, :call
13
+
14
+ define_method(:call) do |**args, &block|
15
+ @jira_client = login
16
+
17
+ fail! Jirawatch.strings.error_login_info_not_found if @jira_client.nil?
18
+
19
+ puts "Connected to #{@jira_client.ServerInfo.all.attrs["baseUrl"]}\n\n"
20
+ send :alias_call, **args
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,16 @@
1
+ module Jirawatch
2
+ module CLI
3
+ class Issues < Dry::CLI::Command
4
+ include Jirawatch::CLI::AuthenticatedCommand
5
+
6
+ argument :project, required: true, desc: "Id or key of the project which issues have to be listed"
7
+
8
+ def call(project: nil, **options)
9
+ puts "Id\t\tKey\t\tType\t\tSummary\n\n"
10
+ @jira_client.Project.find(project).issues.each do |issue|
11
+ puts "#{issue.id}\t\t#{issue.key}\t\t#{issue.fields['issuetype']['name']}\t\t#{issue.fields['summary']}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'jirawatch/jira/provisioning'
2
+
3
+ module Jirawatch
4
+ module CLI
5
+ class Login < Dry::CLI::Command
6
+ include Jirawatch::Jira::Provisioning
7
+
8
+ def call(*)
9
+ puts "Enter your Jira URL (eg. https://veryimportantcompany.atlassian.net)"
10
+ site = STDIN.gets.chomp
11
+ puts "Enter your jira email/username: "
12
+ name = STDIN.gets.chomp
13
+ puts "Enter your API auth token: "
14
+ token = STDIN.gets.chomp
15
+
16
+ unless login name, token, site
17
+ puts "Login failed, no credentials have been saved"
18
+ return
19
+ end
20
+
21
+ save_credentials name, token, site
22
+ puts "Login successful, credentials have been saved"
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,15 @@
1
+ module Jirawatch
2
+ module CLI
3
+ class Projects < Dry::CLI::Command
4
+ include Jirawatch::CLI::AuthenticatedCommand
5
+
6
+ def call(*)
7
+ puts "Id\tKey\tName\n\n"
8
+ @jira_client.Project.all.each do |project|
9
+ puts "#{project.id}\t#{project.key}\t#{project.name}"
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ require 'tty-editor'
2
+
3
+ module Jirawatch
4
+ module CLI
5
+ class Track < Dry::CLI::Command
6
+ include Jirawatch::CLI::AuthenticatedCommand
7
+
8
+ argument :issue_key, required: true, desc: "Issue key that you want to track time for"
9
+ option :tracking_started_at, aliases: ["-t"], desc: "Specify when you started working on this issue with a HH:mm format"
10
+ option :worklog_message, aliases: ["-m"], desc: "Specify a work log message"
11
+
12
+ def initialize(
13
+ reader: TTY::Reader.new,
14
+ worklogger: Jirawatch::Interactors::Worklogger.new
15
+ )
16
+ @reader = reader
17
+ @worklogger = worklogger
18
+ end
19
+
20
+ def call(issue_key:, **options)
21
+ tracking_started_at = Time.parse(options.fetch(:tracking_started_at, Time.now.to_s))
22
+ fail! Jirawatch.strings.error_invalid_tracking_start if tracking_started_at > Time.now
23
+
24
+ issue = @jira_client.Issue.find(issue_key) # Fails if issue doesn't exist
25
+
26
+ paused = false
27
+ tracking_restarted_at = tracking_started_at.to_i
28
+ partial_time_spent = 0
29
+
30
+ worked_hours = (issue.attrs["fields"]["timetracking"]["timeSpentSeconds"]/60)/60
31
+ worked_minutes = (issue.attrs["fields"]["timetracking"]["timeSpentSeconds"]/60)%60
32
+ estimate_hours = (issue.attrs["fields"]["timetracking"]["originalEstimateSeconds"]/60)/60
33
+ estimate_minutes = (issue.attrs["fields"]["timetracking"]["originalEstimateSeconds"]/60)%60
34
+ puts Jirawatch.strings.tracking_cli_name % [issue_key, issue.attrs["fields"]["summary"]]
35
+ puts Jirawatch.strings.tracking_cli_time % [tracking_started_at.strftime("%Y-%m-%d %H:%M:%S"), worked_hours, worked_minutes, estimate_hours, estimate_minutes]
36
+ puts Jirawatch.strings.tracking_cli_inputs
37
+
38
+ @reader.on(:keyctrl_p) do
39
+ unless paused
40
+ partial_time_spent += Time.now.to_i - tracking_restarted_at
41
+ time_unit = partial_time_spent / 60 == 1 ? "minute" : "minutes"
42
+ puts Jirawatch.strings.tracking_paused % [partial_time_spent / 60, time_unit]
43
+ else
44
+ tracking_restarted_at = Time.now.to_i
45
+ puts Jirawatch.strings.tracking_restarted
46
+ end
47
+ paused = !paused
48
+ end
49
+
50
+ begin
51
+ loop do
52
+ @reader.read_line("")
53
+ end
54
+ rescue Interrupt
55
+ total_time_spent = partial_time_spent + (Time.now.to_i - tracking_restarted_at) unless paused
56
+ total_time_spent = partial_time_spent if paused
57
+ @worklogger.call @jira_client, issue_key, total_time_spent, tracking_started_at, default_message: options.fetch(:worklog_message, "")
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ require "dry/cli"
2
+ require 'jirawatch/info'
3
+
4
+ module Jirawatch
5
+ module CLI
6
+ class Version < Dry::CLI::Command
7
+ def call(*)
8
+ puts Jirawatch::Info::VERSION
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Jirawatch
2
+ class Config
3
+
4
+ attr_accessor :template_worklog_file
5
+ attr_accessor :config_path
6
+ attr_accessor :login_file
7
+ attr_accessor :tracking_file_content
8
+
9
+ def initialize
10
+ @template_worklog_file = "/tmp/jirawatch-%s"
11
+ @config_path = File.expand_path "~/.jirawatch"
12
+ @login_file = File.expand_path "~/.jirawatch/access"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Jirawatch
2
+ module Errors
3
+ class CommandFailed < StandardError
4
+ def initialize(msg="Error encountered")
5
+ super(msg)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Jirawatch
2
+ module Info
3
+ VERSION='0.5.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,40 @@
1
+ module Jirawatch
2
+ module Interactors
3
+ class Worklogger
4
+ def initialize (editor: TTY::Editor)
5
+ @editor = editor
6
+ end
7
+
8
+ def call(jira_client, issue_key, total_time_spent, tracking_started_at, default_message: "")
9
+ # Jira work logs have minute sensitivity thus API calls will fail with a time spent
10
+ # which is less than 60 seconds
11
+ fail! Jirawatch.strings.tracking_less_than_60_secs if total_time_spent < 60
12
+
13
+ worklog_file = Jirawatch.configuration.template_worklog_file % tracking_started_at.to_i
14
+ worklog_lines = []
15
+
16
+ File.write worklog_file, default_message
17
+ @editor.open(
18
+ worklog_file,
19
+ content: Jirawatch.strings.tracking_file_content % (total_time_spent / 60)
20
+ ) if default_message.empty?
21
+
22
+ File.readlines(Jirawatch.configuration.template_worklog_file % tracking_started_at.to_i).each do |line|
23
+ worklog_lines << line unless line.start_with?("#") or line.strip.empty?
24
+ end
25
+
26
+ jira_client.Issue.find(issue_key).worklogs.build.save(
27
+ {
28
+ timeSpentSeconds: total_time_spent,
29
+ started: tracking_started_at.strftime("%Y-%m-%dT%H:%M:%S.%L%z"),
30
+ comment: worklog_lines.join("\n")
31
+ }
32
+ ) unless worklog_lines.empty?
33
+
34
+ puts Jirawatch.strings.error_empty_worklog if worklog_lines.empty?
35
+
36
+ File.delete worklog_file
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ require 'jira-ruby'
2
+
3
+ module Jirawatch
4
+ module Jira
5
+ module Provisioning
6
+
7
+ def login(username = nil, token = nil, site = nil)
8
+ if username.nil? or token.nil? or site.nil?
9
+
10
+ return nil unless File.exist? Jirawatch.configuration.login_file
11
+
12
+ File.open(Jirawatch.configuration.login_file).each_line do |line|
13
+ binding.local_variable_set line.split(' ')[0], line.split(' ')[1]
14
+ end
15
+
16
+ end
17
+
18
+ options = {
19
+ username: username,
20
+ password: token,
21
+ site: site,
22
+ context_path: '',
23
+ auth_type: :basic,
24
+ read_timeout: 120
25
+ }
26
+
27
+ client = JIRA::Client.new(options)
28
+ begin
29
+ # Get some infos to check if login was successful
30
+ client.ServerInfo.all.attrs["baseUrl"]
31
+ return client
32
+ rescue StandardError => e
33
+ puts e.message
34
+ end
35
+
36
+ nil
37
+ end
38
+
39
+ def save_credentials(username, token, site)
40
+ File.open(Jirawatch.configuration.login_file, "w") do |f|
41
+ f.write ["username #{username}", "token #{token}", "site #{site}"].join "\n"
42
+ end
43
+ File.chmod 0600, Jirawatch.configuration.login_file
44
+ end
45
+ end
46
+ end
47
+ end