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.
- 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
|