jirawatch 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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