fewald-worklog 0.2.37 → 0.3.2
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 +4 -4
- data/.version +1 -1
- data/lib/cli.rb +6 -0
- data/lib/configuration.rb +72 -1
- data/lib/github/client.rb +172 -0
- data/lib/github/pull_request_details.rb +13 -0
- data/lib/github/pull_request_event.rb +97 -0
- data/lib/github/pull_request_review_event.rb +63 -0
- data/lib/github/push_event.rb +21 -0
- data/lib/log_entry.rb +34 -14
- data/lib/person.rb +2 -2
- data/lib/printer.rb +9 -3
- data/lib/project_storage.rb +5 -0
- data/lib/storage.rb +3 -0
- data/lib/worklog.rb +5 -7
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 484130ef7f6c7cd05ae5dc16c5dc1b69efd05ed3118a085cbf8a1b0c63ab903a
|
|
4
|
+
data.tar.gz: 4d58528e9469766ed8c16d8d86c917c0a19276e4a228f0d38f9ecf4877322d39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cd48303d01eb81d866b274e2fcd4c4adbf95145848d7fb593cf0428162264a578ad00bb7e4732f62f30938d730563914a1e97ff6997eb33d0f31b4f06317866
|
|
7
|
+
data.tar.gz: cacfeb6c3b558ccfe8964f5faba0ec6512f047b625b8b5993294e5f7682193f775fcf4cf2d3dfedc5d6c67e8ce38a85d6f234a7946844cd57a315e661c3ee196
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2
|
|
1
|
+
0.3.2
|
data/lib/cli.rb
CHANGED
|
@@ -71,6 +71,12 @@ class WorklogCLI < Thor
|
|
|
71
71
|
worklog.edit(options)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
desc 'github', 'Fetch latest events from GitHub for a specified user and add them to the work log.'
|
|
75
|
+
def github
|
|
76
|
+
worklog = Worklog::Worklog.new
|
|
77
|
+
worklog.github(options)
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
desc 'remove', 'Remove last entry from the log'
|
|
75
81
|
option :date, type: :string, default: Time.now.strftime('%Y-%m-%d')
|
|
76
82
|
def remove
|
data/lib/configuration.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tzinfo'
|
|
3
4
|
require 'worklogger'
|
|
4
5
|
require 'yaml'
|
|
5
6
|
|
|
@@ -13,9 +14,72 @@ module Worklog
|
|
|
13
14
|
# @!attribute [rw] webserver_port
|
|
14
15
|
# @return [Integer] The port on which the web server runs.
|
|
15
16
|
# Default is 3000.
|
|
17
|
+
# @!attribute [rw] project
|
|
18
|
+
# @return [Configuration::ProjectConfig] Project related configuration.
|
|
19
|
+
# @!attribute [rw] github
|
|
20
|
+
# @return [Configuration::GithubConfig] Github related configuration.
|
|
21
|
+
#
|
|
22
|
+
# @example Example ~/.worklog.yaml
|
|
23
|
+
# storage_path: /Users/username/.worklog
|
|
24
|
+
# log_level: debug
|
|
25
|
+
# timezone: 'America/Los_Angeles'
|
|
26
|
+
# webserver_port: 4000
|
|
27
|
+
#
|
|
28
|
+
# project:
|
|
29
|
+
# show_last: 3
|
|
30
|
+
#
|
|
31
|
+
# github:
|
|
32
|
+
# api_key: 123abc
|
|
33
|
+
# username: sample-user
|
|
16
34
|
class Configuration
|
|
17
|
-
attr_accessor :storage_path, :log_level, :webserver_port
|
|
35
|
+
attr_accessor :storage_path, :log_level, :timezone, :webserver_port, :project, :github
|
|
18
36
|
|
|
37
|
+
# Configuration for projects
|
|
38
|
+
# @!attribute [rw] show_last
|
|
39
|
+
# @return [Integer] Number of last projects to show in the project list.
|
|
40
|
+
class ProjectConfig
|
|
41
|
+
attr_accessor :show_last
|
|
42
|
+
|
|
43
|
+
# Initialize with default values, parameters can be overridden via hash
|
|
44
|
+
# @example
|
|
45
|
+
# ProjectConfig.new({'show_last' => 5})
|
|
46
|
+
def initialize(params = {})
|
|
47
|
+
return if params.nil?
|
|
48
|
+
|
|
49
|
+
params.each do |key, value|
|
|
50
|
+
instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Configuration for Github API access.
|
|
56
|
+
# @!attribute [rw] api_key
|
|
57
|
+
# @return [String] The API key for Github access.
|
|
58
|
+
# @!attribute [rw] username
|
|
59
|
+
# @return [String] The Github username.
|
|
60
|
+
class GithubConfig
|
|
61
|
+
attr_accessor :api_key, :username
|
|
62
|
+
|
|
63
|
+
# Initialize with default values, parameters can be overridden via hash
|
|
64
|
+
# @example
|
|
65
|
+
# GithubConfig.new({'api_key' => '123abc', 'username' => 'sample-user'})
|
|
66
|
+
def initialize(params = {})
|
|
67
|
+
return if params.nil?
|
|
68
|
+
|
|
69
|
+
params.each do |key, value|
|
|
70
|
+
instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Initialize configuration with optional block for setting attributes.
|
|
76
|
+
# If no block is given, default values are used.
|
|
77
|
+
# @example
|
|
78
|
+
# Configuration.new do |config|
|
|
79
|
+
# config.storage_path = '/custom/path'
|
|
80
|
+
# config.log_level = :debug
|
|
81
|
+
# config.timezone = 'America/Los_Angeles'
|
|
82
|
+
# end
|
|
19
83
|
def initialize(&block)
|
|
20
84
|
block.call(self) if block_given?
|
|
21
85
|
|
|
@@ -23,7 +87,11 @@ module Worklog
|
|
|
23
87
|
@storage_path ||= File.join(Dir.home, '.worklog')
|
|
24
88
|
@log_level = log_level || :info
|
|
25
89
|
@log_level = @log_level.to_sym if @log_level.is_a?(String)
|
|
90
|
+
@timezone ||= 'America/Los_Angeles'
|
|
91
|
+
@timezone = TZInfo::Timezone.get(@timezone) if @timezone.is_a?(String)
|
|
26
92
|
@webserver_port ||= 3000
|
|
93
|
+
@project = ProjectConfig.new
|
|
94
|
+
@github ||= GithubConfig.new
|
|
27
95
|
end
|
|
28
96
|
|
|
29
97
|
# Load configuration from a YAML file in the user's home directory.
|
|
@@ -37,6 +105,9 @@ module Worklog
|
|
|
37
105
|
config.storage_path = file_cfg['storage_path'] if file_cfg['storage_path']
|
|
38
106
|
config.log_level = file_cfg['log_level'].to_sym if file_cfg['log_level']
|
|
39
107
|
config.webserver_port = file_cfg['webserver_port'] if file_cfg['webserver_port']
|
|
108
|
+
|
|
109
|
+
config.project = ProjectConfig.new(file_cfg['project'])
|
|
110
|
+
config.github = GithubConfig.new(file_cfg['github'])
|
|
40
111
|
else
|
|
41
112
|
WorkLogger.debug "Configuration file does not exist in #{file_path}, using defaults."
|
|
42
113
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
require 'github/pull_request_details'
|
|
5
|
+
require 'github/pull_request_event'
|
|
6
|
+
require 'github/pull_request_review_event'
|
|
7
|
+
require 'github/push_event'
|
|
8
|
+
require 'worklogger'
|
|
9
|
+
|
|
10
|
+
module Worklog
|
|
11
|
+
module Github
|
|
12
|
+
# Client to interact with GitHub API
|
|
13
|
+
class Client
|
|
14
|
+
class GithubAPIError < StandardError; end
|
|
15
|
+
|
|
16
|
+
EVENT_FILTER = Set.new(%w[
|
|
17
|
+
PullRequestEvent
|
|
18
|
+
PullRequestReviewEvent
|
|
19
|
+
|
|
20
|
+
]).freeze
|
|
21
|
+
|
|
22
|
+
def initialize(configuration)
|
|
23
|
+
@configuration = configuration
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fetch events for a given user from Github API
|
|
27
|
+
def fetch_events
|
|
28
|
+
verify_token!
|
|
29
|
+
|
|
30
|
+
WorkLogger.debug 'Fetching most recent GitHub events...'
|
|
31
|
+
responses = fetch_event_pages
|
|
32
|
+
responses.filter_map { |event| create_event(event) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def pull_request_data(repo, pr_number)
|
|
36
|
+
response = HTTParty.get("https://api.github.com/repos/#{repo}/pulls/#{pr_number}",
|
|
37
|
+
headers: { 'Authorization' => "token #{TOKEN}" })
|
|
38
|
+
response.parsed_response
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pull_request_comments(repo, pr_number)
|
|
42
|
+
response = HTTParty.get("https://api.github.com/repos/#{repo}/pulls/#{pr_number}/comments",
|
|
43
|
+
headers: { 'Authorization' => "token #{TOKEN}" })
|
|
44
|
+
response.parsed_response
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def create_event(event)
|
|
50
|
+
return unless EVENT_FILTER.include?(event['type'])
|
|
51
|
+
|
|
52
|
+
case event['type']
|
|
53
|
+
when 'PullRequestEvent'
|
|
54
|
+
create_pull_request_event(event)
|
|
55
|
+
when 'PullRequestReviewEvent'
|
|
56
|
+
create_pull_request_review_event(event)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_pull_request_event(event)
|
|
61
|
+
payload = event['payload']
|
|
62
|
+
repo = event['repo']
|
|
63
|
+
|
|
64
|
+
# Retrieve details for the specific pull request
|
|
65
|
+
pr_details = pull_request_details(repo['name'], payload['number'])
|
|
66
|
+
|
|
67
|
+
PullRequestEvent.new(
|
|
68
|
+
repository: repo['name'],
|
|
69
|
+
number: payload['number'],
|
|
70
|
+
**pr_details.to_h.slice(:url, :title, :description, :created_at, :merged_at, :closed_at)
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def create_pull_request_review_event(event)
|
|
75
|
+
repo_name = event['repo']['name']
|
|
76
|
+
payload = event['payload']
|
|
77
|
+
pr_number = payload['pull_request']['number']
|
|
78
|
+
|
|
79
|
+
review = payload['review']
|
|
80
|
+
review_state = review['state']
|
|
81
|
+
url = review['html_url']
|
|
82
|
+
created_at = to_local_time(review['submitted_at'])
|
|
83
|
+
|
|
84
|
+
pr_details = pull_request_details(repo_name, pr_number)
|
|
85
|
+
|
|
86
|
+
PullRequestReviewEvent.new(
|
|
87
|
+
repository: repo_name,
|
|
88
|
+
number: pr_number,
|
|
89
|
+
url:,
|
|
90
|
+
title: pr_details.title,
|
|
91
|
+
description: pr_details.description,
|
|
92
|
+
creator: pr_details.creator,
|
|
93
|
+
state: review_state,
|
|
94
|
+
created_at: created_at
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get detailed information about a specific pull request
|
|
99
|
+
# @param repo [String] Repository name in the format 'owner/repo'
|
|
100
|
+
# @param pr_number [Integer] Pull request number
|
|
101
|
+
# @return [PullRequestDetails] Struct containing pull request details
|
|
102
|
+
def pull_request_details(repo, pr_number)
|
|
103
|
+
WorkLogger.debug "Fetching details for PR ##{pr_number} in #{repo}..."
|
|
104
|
+
response = HTTParty.get("https://api.github.com/repos/#{repo}/pulls/#{pr_number}",
|
|
105
|
+
headers: { 'Authorization' => "token #{@configuration.github.api_key}" })
|
|
106
|
+
|
|
107
|
+
if response.code == 403
|
|
108
|
+
raise GithubAPIError,
|
|
109
|
+
'Failed to fetch PR details: Are you connected to the corporate VPN? (HTTP 403 Forbidden)'
|
|
110
|
+
elsif response.code != 200
|
|
111
|
+
raise GithubAPIError, "Failed to fetch PR details: HTTPCode #{response.code}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
pr = response.parsed_response
|
|
115
|
+
PullRequestDetails.new(
|
|
116
|
+
title: pr['title'],
|
|
117
|
+
description: pr['body'],
|
|
118
|
+
creator: pr['user'] ? pr['user']['login'] : nil,
|
|
119
|
+
url: pr['html_url'],
|
|
120
|
+
state: pr['state'],
|
|
121
|
+
merged: pr['merged'],
|
|
122
|
+
created_at: to_local_time(pr['created_at']),
|
|
123
|
+
merged_at: to_local_time(pr['merged_at']),
|
|
124
|
+
closed_at: to_local_time(pr['closed_at'])
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Generic method to perform GET requests to GitHub API
|
|
129
|
+
def github_api_get(url)
|
|
130
|
+
response = HTTParty.get(url, headers: { 'Authorization' => "token #{@configuration.github.api_key}" })
|
|
131
|
+
raise GithubAPIError, "GitHub API request failed with code #{response.code}" unless response.code == 200
|
|
132
|
+
|
|
133
|
+
# TODO: Respect rate limit headers
|
|
134
|
+
# headers = response.headers
|
|
135
|
+
# puts "Remaining: #{headers['X-RateLimit-Remaining']}"
|
|
136
|
+
# puts "Reset: #{headers['X-RateLimit-Reset']}"
|
|
137
|
+
# puts "Limit: #{headers['X-RateLimit-Limit']}"
|
|
138
|
+
# puts "Used: #{headers['X-RateLimit-Used']}"
|
|
139
|
+
response.parsed_response
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Fetch the maximum number of events with pagination for the configured user
|
|
143
|
+
# @return [Array<Hash>] Array of event hashes
|
|
144
|
+
def fetch_event_pages
|
|
145
|
+
responses = []
|
|
146
|
+
(1..3).each do |page|
|
|
147
|
+
responses += github_api_get("https://api.github.com/users/#{@configuration.github.username}/events?per_page=100&page=#{page}")
|
|
148
|
+
end
|
|
149
|
+
responses
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Convert a DateTime to local time based on configuration timezone
|
|
153
|
+
# @param time [Time, String] The Time to convert
|
|
154
|
+
# @return [Time, nil] The converted Time in local time, or nil if input is nil
|
|
155
|
+
def to_local_time(time)
|
|
156
|
+
return nil if time.nil?
|
|
157
|
+
|
|
158
|
+
time = Time.parse(time) if time.is_a?(String)
|
|
159
|
+
|
|
160
|
+
@configuration.timezone.utc_to_local(time)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Verify that the GitHub API token is present in the configuration
|
|
164
|
+
# @raise [GithubAPIError] if the API token is missing
|
|
165
|
+
def verify_token!
|
|
166
|
+
WorkLogger.debug 'Verifying GitHub API token presence'
|
|
167
|
+
@configuration.github.api_key || raise(GithubAPIError,
|
|
168
|
+
'GitHub API key is not configured. Please set it in the configuration.')
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Worklog
|
|
4
|
+
module Github
|
|
5
|
+
# Details of a pull request, used internally
|
|
6
|
+
PullRequestDetails = Struct.new('PullRequestDetails', :title, :description, :creator, :url, :state, :merged,
|
|
7
|
+
:created_at, :merged_at, :closed_at, keyword_init: true) do
|
|
8
|
+
def merged?
|
|
9
|
+
state == 'closed' && merged == true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hasher'
|
|
4
|
+
require 'log_entry'
|
|
5
|
+
require 'worklogger'
|
|
6
|
+
|
|
7
|
+
module Worklog
|
|
8
|
+
module Github
|
|
9
|
+
# An event representing a pull request
|
|
10
|
+
# @!attribute [rw] repository
|
|
11
|
+
# @return [String] the repository name
|
|
12
|
+
# @!attribute [rw] number
|
|
13
|
+
# @return [Integer] the pull request number
|
|
14
|
+
# @!attribute [rw] url
|
|
15
|
+
# @return [String] the URL of the pull request
|
|
16
|
+
# @!attribute [rw] title
|
|
17
|
+
# @return [String] the title of the pull request
|
|
18
|
+
# @!attribute [rw] description
|
|
19
|
+
# @return [String] the description of the pull request
|
|
20
|
+
# @!attribute [rw] created_at
|
|
21
|
+
# @return [Time] the creation time of the pull request
|
|
22
|
+
# @!attribute [rw] merged_at
|
|
23
|
+
# @return [Time, nil] the time the pull request was merged, or nil if not merged
|
|
24
|
+
# @!attribute [rw] closed_at
|
|
25
|
+
# @return [Time, nil] the time the pull request was closed, or nil if not closed
|
|
26
|
+
class PullRequestEvent
|
|
27
|
+
attr_accessor :repository, :number, :url, :title, :description, :created_at, :merged_at, :closed_at
|
|
28
|
+
|
|
29
|
+
def initialize(params = {})
|
|
30
|
+
params.each do |key, value|
|
|
31
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns true if the pull request was merged.
|
|
36
|
+
# Usually, a merged pull request is also closed.
|
|
37
|
+
# @return [Boolean] true if merged, false otherwise
|
|
38
|
+
def merged?
|
|
39
|
+
!merged_at.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns true if the pull request was closed.
|
|
43
|
+
# A closed pull request may or may not be merged.
|
|
44
|
+
# @return [Boolean] true if closed, false otherwise
|
|
45
|
+
# @see #merged?
|
|
46
|
+
def closed?
|
|
47
|
+
# Treat merged pull requests as closed
|
|
48
|
+
!closed_at.nil? || (merged? && closed_at.nil?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convert the PullRequestEvent to a LogEntry
|
|
52
|
+
# @return [LogEntry]
|
|
53
|
+
def to_log_entry
|
|
54
|
+
message = if merged?
|
|
55
|
+
'Merged PR '
|
|
56
|
+
elsif closed?
|
|
57
|
+
'Closed PR '
|
|
58
|
+
else
|
|
59
|
+
'Created PR '
|
|
60
|
+
end
|
|
61
|
+
message += title
|
|
62
|
+
|
|
63
|
+
# If merged, use merged_at time; if closed, use closed_at time; otherwise, use created_at time
|
|
64
|
+
time = if merged?
|
|
65
|
+
merged_at
|
|
66
|
+
elsif closed?
|
|
67
|
+
closed_at
|
|
68
|
+
else
|
|
69
|
+
created_at
|
|
70
|
+
end
|
|
71
|
+
key = Hasher.sha256("#{repository}#{number}#{merged?}#{closed?}")
|
|
72
|
+
LogEntry.new(
|
|
73
|
+
key:,
|
|
74
|
+
source: 'github',
|
|
75
|
+
time:,
|
|
76
|
+
message:,
|
|
77
|
+
url: url,
|
|
78
|
+
epic: false,
|
|
79
|
+
ticket: nil
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# String representation of the PullRequestEvent
|
|
84
|
+
# @return [String]
|
|
85
|
+
def to_s
|
|
86
|
+
short_url = url.length > 10 ? "...#{url[-10..]}" : url
|
|
87
|
+
unless description.nil?
|
|
88
|
+
short_description = description.gsub(/\n+/, ' ')
|
|
89
|
+
short_description = "#{short_description[0..16]}..." if short_description.length > 20
|
|
90
|
+
end
|
|
91
|
+
"#<PullRequestEvent repository=#{repository} number=#{number} url=#{short_url} title=#{title} " \
|
|
92
|
+
"description=#{short_description} " \
|
|
93
|
+
"created_at=#{created_at} merged_at=#{merged_at} closed_at=#{closed_at}>"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hasher'
|
|
4
|
+
require 'log_entry'
|
|
5
|
+
require 'worklogger'
|
|
6
|
+
|
|
7
|
+
module Worklog
|
|
8
|
+
module Github
|
|
9
|
+
# Event representing a pull request review
|
|
10
|
+
# @!attribute [rw] repository
|
|
11
|
+
# @return [String] the repository name
|
|
12
|
+
# @!attribute [rw] number
|
|
13
|
+
# @return [Integer] the pull request number
|
|
14
|
+
# @!attribute [rw] url
|
|
15
|
+
# @return [String] the URL of the pull request review
|
|
16
|
+
# @!attribute [rw] title
|
|
17
|
+
# @return [String] the title of the pull request
|
|
18
|
+
# @!attribute [rw] description
|
|
19
|
+
# @return [String] the description of the pull request
|
|
20
|
+
# @!attribute [rw] creator
|
|
21
|
+
# @return [String] the username of the pull request creator
|
|
22
|
+
# @!attribute [rw] created_at
|
|
23
|
+
# @return [Time] the creation time of the pull request review, not the pull request itself
|
|
24
|
+
# @!attribute [rw] state
|
|
25
|
+
# @return [String] the state of the review (e.g., 'approved', 'changes_requested', etc.)
|
|
26
|
+
class PullRequestReviewEvent
|
|
27
|
+
attr_accessor :repository, :number, :url, :title, :description, :creator, :created_at, :state
|
|
28
|
+
|
|
29
|
+
def initialize(params = {})
|
|
30
|
+
params.each do |key, value|
|
|
31
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether the pull request review was approved
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def approved?
|
|
38
|
+
state.downcase == 'approved'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Convert the PullRequestReviewEvent to a LogEntry
|
|
42
|
+
# @return [LogEntry]
|
|
43
|
+
def to_log_entry
|
|
44
|
+
message = String.new 'Reviewed '
|
|
45
|
+
message << 'and approved ' if approved?
|
|
46
|
+
message << "PR ##{number} #{creator}: #{title}"
|
|
47
|
+
LogEntry.new(
|
|
48
|
+
key: Hasher.sha256("#{repository}-#{number}-#{state}"),
|
|
49
|
+
source: 'github',
|
|
50
|
+
time: created_at,
|
|
51
|
+
message: message,
|
|
52
|
+
url: url
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# String representation of the PullRequestReviewEvent
|
|
57
|
+
# @return [String]
|
|
58
|
+
def to_s
|
|
59
|
+
"#<PullRequestReviewEvent repository=#{repository} number=#{number} state=#{state} creator=#{creator} created_at=#{created_at}>" # rubocop:disable Layout/LineLength
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'log_entry'
|
|
4
|
+
|
|
5
|
+
module Worklog
|
|
6
|
+
module Github
|
|
7
|
+
# Event representing a push event
|
|
8
|
+
class PushEvent
|
|
9
|
+
def to_log_entry
|
|
10
|
+
WorkLogger.debug('Converting PushEvent to LogEntry')
|
|
11
|
+
LogEntry.new(
|
|
12
|
+
key: 'github-push-event',
|
|
13
|
+
source: 'github',
|
|
14
|
+
time: Time.now, # TODO: Fix this
|
|
15
|
+
message: 'Push event occurred',
|
|
16
|
+
url: nil
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/log_entry.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Worklog
|
|
|
10
10
|
# @see DailyLog
|
|
11
11
|
# @!attribute [rw] key
|
|
12
12
|
# @return [String] the unique key of the log entry. The key is generated based on the time and message.
|
|
13
|
+
# @!attribute [rw] source
|
|
14
|
+
# @return [String] the source of the log entry, e.g., 'github', 'manual', etc.
|
|
13
15
|
# @!attribute [rw] time
|
|
14
16
|
# @return [Time] the date and time of the log entry.
|
|
15
17
|
# @!attribute [rw] tags
|
|
@@ -29,14 +31,15 @@ module Worklog
|
|
|
29
31
|
|
|
30
32
|
include Hashify
|
|
31
33
|
|
|
32
|
-
attr_accessor :key, :time, :tags, :ticket, :url, :epic, :message, :project
|
|
34
|
+
attr_accessor :key, :source, :time, :tags, :ticket, :url, :epic, :message, :project
|
|
33
35
|
|
|
34
36
|
attr_reader :day
|
|
35
37
|
|
|
36
38
|
def initialize(params = {})
|
|
37
39
|
# key can be nil. This is needed for backwards compatibility with older log entries.
|
|
38
40
|
@key = params[:key]
|
|
39
|
-
@
|
|
41
|
+
@source = params[:source] || 'manual'
|
|
42
|
+
@time = params[:time].is_a?(String) ? Time.parse(params[:time]) : params[:time]
|
|
40
43
|
# If tags are nil, set to empty array.
|
|
41
44
|
# This is similar to the CLI default value.
|
|
42
45
|
@tags = params[:tags] || []
|
|
@@ -72,24 +75,21 @@ module Worklog
|
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
s =
|
|
78
|
+
s = String.new
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
# Prefix with [EPIC] if epic
|
|
81
|
+
s << epic_prefix if epic?
|
|
82
|
+
|
|
83
|
+
# Print the message
|
|
84
|
+
s << if source == 'github'
|
|
85
|
+
Rainbow(msg).fg(:green)
|
|
79
86
|
else
|
|
80
87
|
msg
|
|
81
88
|
end
|
|
82
89
|
|
|
83
|
-
s
|
|
84
|
-
|
|
85
|
-
# Add tags in brackets if defined.
|
|
86
|
-
s += ' [' + @tags.map { |tag| "#{tag}" }.join(', ') + ']' if @tags && @tags.size > 0
|
|
87
|
-
|
|
88
|
-
# Add URL in brackets if defined.
|
|
89
|
-
s += " [#{@url}]" if @url && @url != ''
|
|
90
|
-
|
|
91
|
-
s += " [#{@project}]" if @project && @project != ''
|
|
90
|
+
s << " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
|
|
92
91
|
|
|
92
|
+
s << format_metadata
|
|
93
93
|
s
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -130,5 +130,25 @@ module Worklog
|
|
|
130
130
|
time == other.time && tags == other.tags && ticket == other.ticket && url == other.url &&
|
|
131
131
|
epic == other.epic && message == other.message
|
|
132
132
|
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Prefix for epic entries with formatting.
|
|
137
|
+
# @return [String]
|
|
138
|
+
def epic_prefix
|
|
139
|
+
"#{Rainbow('[EPIC]').bg(:white).fg(:black)} "
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Format metadata for display.
|
|
143
|
+
# @return [String]
|
|
144
|
+
def format_metadata
|
|
145
|
+
metadata_parts = []
|
|
146
|
+
metadata_parts << Rainbow(@ticket).fg(:blue) if @ticket
|
|
147
|
+
metadata_parts << @tags.join(', ') if @tags&.any?
|
|
148
|
+
metadata_parts << @url if @url && @url != ''
|
|
149
|
+
metadata_parts << @project if @project && @project != ''
|
|
150
|
+
|
|
151
|
+
metadata_parts.empty? ? '' : " [#{metadata_parts.join('] [')}]"
|
|
152
|
+
end
|
|
133
153
|
end
|
|
134
154
|
end
|
data/lib/person.rb
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
class Person
|
|
16
16
|
attr_reader :handle, :name, :email, :team, :notes
|
|
17
17
|
|
|
18
|
-
def initialize(handle
|
|
18
|
+
def initialize(handle:, name:, email:, team:, notes: [])
|
|
19
19
|
@handle = handle
|
|
20
20
|
@name = name
|
|
21
21
|
@email = email
|
|
@@ -40,7 +40,7 @@ class Person
|
|
|
40
40
|
email = hash[:email] || hash['email']
|
|
41
41
|
team = hash[:team] || hash['team']
|
|
42
42
|
notes = hash[:notes] || hash['notes'] || []
|
|
43
|
-
Person.new(handle, name, email, team, notes)
|
|
43
|
+
Person.new(handle: handle, name: name, email: email, team: team, notes: notes)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def to_s
|
data/lib/printer.rb
CHANGED
|
@@ -7,8 +7,10 @@ class Printer
|
|
|
7
7
|
attr_reader :people
|
|
8
8
|
|
|
9
9
|
# Initializes the printer with a list of people.
|
|
10
|
+
# @param configuration [Configuration] The configuration.
|
|
10
11
|
# @param people [Array<Person>] An array of Person objects.
|
|
11
|
-
def initialize(people = nil)
|
|
12
|
+
def initialize(configuration, people = nil)
|
|
13
|
+
@configuration = configuration
|
|
12
14
|
@people = people || {}
|
|
13
15
|
end
|
|
14
16
|
|
|
@@ -58,14 +60,18 @@ class Printer
|
|
|
58
60
|
# @param entry [LogEntry]
|
|
59
61
|
# @param date_inline [Boolean] If true, the date is printed inline with the time.
|
|
60
62
|
def print_entry(daily_log, entry, date_inline = false)
|
|
61
|
-
|
|
63
|
+
# Backwards compatibility: convert strings to Date/Time objects if necessary
|
|
64
|
+
entry.time = Time.strptime(entry.time, '%H:%M:%S') unless entry.time.respond_to?(:strftime)
|
|
65
|
+
|
|
66
|
+
# Convert to local time zone
|
|
67
|
+
entry.time = entry.time.getlocal(@configuration.timezone) if @configuration.timezone
|
|
62
68
|
|
|
63
69
|
time_string = if date_inline
|
|
64
70
|
"#{daily_log.date.strftime('%a, %Y-%m-%d')} #{entry.time.strftime('%H:%M')}"
|
|
65
71
|
else
|
|
66
72
|
entry.time.strftime('%H:%M')
|
|
67
73
|
end
|
|
68
|
-
|
|
74
|
+
print ' ' unless date_inline
|
|
69
75
|
puts "#{Rainbow(time_string).gold} #{entry.message_string(@people)}"
|
|
70
76
|
end
|
|
71
77
|
end
|
data/lib/project_storage.rb
CHANGED
|
@@ -71,6 +71,11 @@ module Worklog
|
|
|
71
71
|
projects.key?(key)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# Alias for exist? method.
|
|
75
|
+
# @param key [String] The key of the project to check.
|
|
76
|
+
# @return [Boolean] Returns true if the project exists, false otherwise.
|
|
77
|
+
def key?(key) = exist?(key)
|
|
78
|
+
|
|
74
79
|
private
|
|
75
80
|
|
|
76
81
|
# Check whether projects.yaml exists in the project_dir
|
data/lib/storage.rb
CHANGED
data/lib/worklog.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'yaml'
|
|
|
8
8
|
require 'configuration'
|
|
9
9
|
require 'daily_log'
|
|
10
10
|
require 'date_parser'
|
|
11
|
+
require 'github/client'
|
|
11
12
|
require 'hash'
|
|
12
13
|
require 'hasher'
|
|
13
14
|
require 'log_entry'
|
|
@@ -98,9 +99,6 @@ module Worklog
|
|
|
98
99
|
url: options[:url], epic: options[:epic], message:, project: options[:project])
|
|
99
100
|
daily_log << new_entry
|
|
100
101
|
|
|
101
|
-
# Sort by time in case an entry was added later out of order.
|
|
102
|
-
daily_log.entries.sort_by!(&:time)
|
|
103
|
-
|
|
104
102
|
@storage.write_log(@storage.filepath(options[:date]), daily_log)
|
|
105
103
|
|
|
106
104
|
(new_entry.people - @people.keys).each do |handle|
|
|
@@ -144,7 +142,7 @@ module Worklog
|
|
|
144
142
|
# worklog.show(from: '2023-10-01', to: '2023-10-31')
|
|
145
143
|
# worklog.show(date: '2023-10-01')
|
|
146
144
|
def show(options = {})
|
|
147
|
-
printer = Printer.new(@people)
|
|
145
|
+
printer = Printer.new(@config, @people)
|
|
148
146
|
|
|
149
147
|
start_date, end_date = start_end_date(options)
|
|
150
148
|
|
|
@@ -194,7 +192,7 @@ module Worklog
|
|
|
194
192
|
end
|
|
195
193
|
|
|
196
194
|
def person_detail(all_logs, all_people, person)
|
|
197
|
-
printer = Printer.new(all_people)
|
|
195
|
+
printer = Printer.new(@config, all_people)
|
|
198
196
|
puts "All interactions with #{Rainbow(person.name).gold}"
|
|
199
197
|
|
|
200
198
|
if person.notes
|
|
@@ -341,7 +339,7 @@ module Worklog
|
|
|
341
339
|
# @example
|
|
342
340
|
# worklog.tag_detail('example_tag', from: '2023-10-01', to: '2023-10-31')
|
|
343
341
|
def tag_detail(tag, options)
|
|
344
|
-
printer = Printer.new(@people)
|
|
342
|
+
printer = Printer.new(@config, @people)
|
|
345
343
|
start_date, end_date = start_end_date(options)
|
|
346
344
|
|
|
347
345
|
@storage.days_between(start_date, end_date).each do |daily_log|
|
|
@@ -371,7 +369,7 @@ module Worklog
|
|
|
371
369
|
|
|
372
370
|
# Do nothing if no entries are found.
|
|
373
371
|
if entries.empty?
|
|
374
|
-
Printer.new.no_entries(start_date, end_date)
|
|
372
|
+
Printer.new(@config).no_entries(start_date, end_date)
|
|
375
373
|
return
|
|
376
374
|
end
|
|
377
375
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fewald-worklog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Friedrich Ewald
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '1.3'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: tzinfo
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '2.0'
|
|
110
124
|
description: |
|
|
111
125
|
Command line tool for tracking achievments, tasks and interactions.
|
|
112
126
|
|
|
@@ -127,6 +141,11 @@ files:
|
|
|
127
141
|
- lib/daily_log.rb
|
|
128
142
|
- lib/date_parser.rb
|
|
129
143
|
- lib/editor.rb
|
|
144
|
+
- lib/github/client.rb
|
|
145
|
+
- lib/github/pull_request_details.rb
|
|
146
|
+
- lib/github/pull_request_event.rb
|
|
147
|
+
- lib/github/pull_request_review_event.rb
|
|
148
|
+
- lib/github/push_event.rb
|
|
130
149
|
- lib/hash.rb
|
|
131
150
|
- lib/hasher.rb
|
|
132
151
|
- lib/log_entry.rb
|