jirawatch 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/gem-push-gpr.yml +30 -0
- data/.github/workflows/gem-push-rubygems.yml +22 -0
- data/.gitignore +5 -0
- data/Dockerfile +14 -0
- data/Gemfile +3 -0
- data/LICENSE +674 -0
- data/README.md +71 -0
- data/bin/jirawatch +23 -0
- data/jirawatch.gemspec +19 -0
- data/lib/jirawatch.rb +35 -0
- data/lib/jirawatch/cli/authenticated_command.rb +31 -0
- data/lib/jirawatch/cli/issues.rb +16 -0
- data/lib/jirawatch/cli/login.rb +27 -0
- data/lib/jirawatch/cli/projects.rb +15 -0
- data/lib/jirawatch/cli/track.rb +62 -0
- data/lib/jirawatch/cli/version.rb +12 -0
- data/lib/jirawatch/config.rb +15 -0
- data/lib/jirawatch/errors/command_failed.rb +9 -0
- data/lib/jirawatch/info.rb +5 -0
- data/lib/jirawatch/interactors/worklogger.rb +40 -0
- data/lib/jirawatch/jira/provisioning.rb +47 -0
- data/lib/jirawatch/strings.rb +63 -0
- data/lib/jirawatch/utils/messages.rb +11 -0
- metadata +107 -0
data/README.md
ADDED
@@ -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!
|
data/bin/jirawatch
ADDED
@@ -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
|
data/jirawatch.gemspec
ADDED
@@ -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
|
data/lib/jirawatch.rb
ADDED
@@ -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,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,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
|