ftg 2.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/.gitignore +15 -0
- data/.idea/.ftg.iml +31 -0
- data/.idea/misc.xml +14 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +45 -0
- data/.ruby-version +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/ftg +7 -0
- data/bin/setup +7 -0
- data/config/private.json.example +6 -0
- data/config/public.json +30 -0
- data/ftg.gemspec +32 -0
- data/lib/coffee.rb +79 -0
- data/lib/colors.rb +38 -0
- data/lib/ftg.rb +316 -0
- data/lib/ftg_logger.rb +67 -0
- data/lib/ftg_options.rb +28 -0
- data/lib/ftg_stats.rb +135 -0
- data/lib/ftg_sync.rb +112 -0
- data/lib/idle_logger.rb +9 -0
- data/lib/interactive.rb +115 -0
- data/lib/migrations/create_tasks.rb +21 -0
- data/lib/models/task.rb +5 -0
- data/lib/task_formatter.rb +51 -0
- data/lib/utils.rb +24 -0
- metadata +105 -0
data/lib/ftg.rb
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
require_relative './colors'
|
|
2
|
+
require_relative './utils'
|
|
3
|
+
require_relative './ftg_options'
|
|
4
|
+
require_relative './ftg_logger'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
class Ftg
|
|
9
|
+
include FtgOptions
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@commands = {
|
|
13
|
+
help: { fn: -> { help }, aliases: [] },
|
|
14
|
+
gtfo: { fn: -> {
|
|
15
|
+
gtfo(day_option, get_option(['--restore']), get_option(['--reset'])) # option union
|
|
16
|
+
}, aliases: [:recap, :leave, :wrap_up] },
|
|
17
|
+
status: { fn: -> { status }, aliases: [:stack] },
|
|
18
|
+
current: { fn: -> { current }, aliases: [] },
|
|
19
|
+
git_stats: { fn: -> { git_stats }, aliases: [] },
|
|
20
|
+
start: { fn: -> { start(ARGV[1]) }, aliases: [] },
|
|
21
|
+
stop: { fn: -> { stop(get_option(['--all'])) }, aliases: [:end, :pop] },
|
|
22
|
+
pause: { fn: -> { pause }, aliases: [] },
|
|
23
|
+
resume: { fn: -> { resume }, aliases: [] },
|
|
24
|
+
edit: { fn: -> {
|
|
25
|
+
edit(day_option, get_option(['--restore']), get_option(['--reset'])) # option union
|
|
26
|
+
}, aliases: [] },
|
|
27
|
+
list: { fn: -> { list(get_option(['--day', '-d'])) }, aliases: [:ls, :history, :recent] },
|
|
28
|
+
sync: { fn: -> { sync }, aliases: [] },
|
|
29
|
+
config: { fn: -> { config }, aliases: [] },
|
|
30
|
+
touch: { fn: -> { touch(ARGV[1]) }, aliases: [] },
|
|
31
|
+
delete: { fn: -> { delete(ARGV[1]) }, aliases: [:remove] },
|
|
32
|
+
email: { fn: -> { email(day_option) }, aliases: [:mail] },
|
|
33
|
+
migrate: { fn: -> { migrate }, aliases: [] },
|
|
34
|
+
console: { fn: -> { console }, aliases: [:shell] },
|
|
35
|
+
coffee: { fn: -> { coffee(get_option(['--big'])) } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@ftg_dir = "#{ENV['HOME']}/.ftg"
|
|
39
|
+
private_config = JSON.parse(File.open("#{@ftg_dir}/config/private.json", 'r').read)
|
|
40
|
+
public_config = JSON.parse(File.open("#{@ftg_dir}/config/public.json", 'r').read)
|
|
41
|
+
@config = public_config.deep_merge(private_config)
|
|
42
|
+
@ftg_logger = FtgLogger.new(@ftg_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def require_models
|
|
47
|
+
require 'active_record'
|
|
48
|
+
require_relative './models/task'
|
|
49
|
+
require_relative './migrations/create_tasks'
|
|
50
|
+
require_relative './task_formatter'
|
|
51
|
+
|
|
52
|
+
ActiveRecord::Base.establish_connection(
|
|
53
|
+
adapter: 'sqlite3',
|
|
54
|
+
database: 'db/ftg.sqlite3'
|
|
55
|
+
)
|
|
56
|
+
fail('Cannot open task connection') unless Task.connection
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run
|
|
60
|
+
help(1) if ARGV[0].nil?
|
|
61
|
+
cmd = get_command(ARGV[0])
|
|
62
|
+
fail("Unknown command #{ARGV[0]}") if cmd.nil?
|
|
63
|
+
cmd[1][:fn].call
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#####################################################################################
|
|
67
|
+
####################################### COMMANDS ####################################
|
|
68
|
+
#####################################################################################
|
|
69
|
+
|
|
70
|
+
def help(exit_code = 0)
|
|
71
|
+
help = <<-HELP
|
|
72
|
+
Usage: ftg <command> [arguments...]
|
|
73
|
+
By default, the day param is the current day.
|
|
74
|
+
|
|
75
|
+
Command list:
|
|
76
|
+
start, stop, pause, resume <task> Manage tasks
|
|
77
|
+
gtfo Executes: edit, sync, mail
|
|
78
|
+
edit <task> [-d <day>, --reset] Manually edit times
|
|
79
|
+
sync [-d <day>] Sync times with jira and toggl
|
|
80
|
+
mail Send an email
|
|
81
|
+
stats [-d <day>] Show time stats
|
|
82
|
+
current Show current task
|
|
83
|
+
pop Stop current task and resume previous one
|
|
84
|
+
touch <task> Start and end a task right away
|
|
85
|
+
remove <task> Delete a task
|
|
86
|
+
list List of tasks/meetings of the day
|
|
87
|
+
config Show config files
|
|
88
|
+
console Open a console
|
|
89
|
+
HELP
|
|
90
|
+
puts help
|
|
91
|
+
exit(exit_code)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def config
|
|
95
|
+
require 'ap'
|
|
96
|
+
puts 'Settings are in the ./config folder:'
|
|
97
|
+
puts ' public.json default settings. Do not edit manually. Added to git'
|
|
98
|
+
puts ' private.json personal settings. This will overwrite public.json. Ignored in git'
|
|
99
|
+
|
|
100
|
+
puts "\nCurrent config:\n"
|
|
101
|
+
ap @config
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def start(task)
|
|
105
|
+
if task == 'auto' || task == 'current_branch'
|
|
106
|
+
task = `git rev-parse --abbrev-ref HEAD`.strip
|
|
107
|
+
end
|
|
108
|
+
if task.nil? || task == ''
|
|
109
|
+
fail('Enter a task. Eg: ftg start jt-1234')
|
|
110
|
+
end
|
|
111
|
+
if @ftg_logger.on_pause?
|
|
112
|
+
status
|
|
113
|
+
fail("\nCannot start a task while on pause. Use \"ftg resume\" first")
|
|
114
|
+
end
|
|
115
|
+
if @ftg_logger.get_unclosed_logs.find { |l| l[:task_name] == task }
|
|
116
|
+
status
|
|
117
|
+
fail("\nTask #{task} already started")
|
|
118
|
+
end
|
|
119
|
+
@ftg_logger.add_log('ftg_start', task)
|
|
120
|
+
status
|
|
121
|
+
@ftg_logger.update_current
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def stop(all)
|
|
125
|
+
@ftg_logger.get_unclosed_logs.each do |log|
|
|
126
|
+
@ftg_logger.add_log('ftg_stop', log[:task_name])
|
|
127
|
+
break unless all
|
|
128
|
+
end
|
|
129
|
+
status
|
|
130
|
+
@ftg_logger.update_current
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def pause
|
|
134
|
+
if @ftg_logger.on_pause?
|
|
135
|
+
status
|
|
136
|
+
fail("\nAlready on pause")
|
|
137
|
+
end
|
|
138
|
+
@ftg_logger.add_log('ftg_start', 'pause')
|
|
139
|
+
status
|
|
140
|
+
@ftg_logger.update_current
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def resume
|
|
144
|
+
@ftg_logger.add_log('ftg_stop', 'pause')
|
|
145
|
+
status
|
|
146
|
+
@ftg_logger.update_current
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def touch(task)
|
|
150
|
+
@ftg_logger.add_log('ftg_start', task)
|
|
151
|
+
@ftg_logger.add_log('ftg_stop', task)
|
|
152
|
+
status
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def delete(task)
|
|
156
|
+
if task == '--all'
|
|
157
|
+
@ftg_logger.remove_all_logs
|
|
158
|
+
end
|
|
159
|
+
@ftg_logger.remove_logs(task)
|
|
160
|
+
status
|
|
161
|
+
@ftg_logger.update_current
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def gtfo(day, restore, reset)
|
|
165
|
+
edit(day, restore, reset)
|
|
166
|
+
email(day)
|
|
167
|
+
puts "sync soon..."
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def status
|
|
171
|
+
current_logs = @ftg_logger.get_unclosed_logs
|
|
172
|
+
if current_logs.empty?
|
|
173
|
+
puts 'No current task'
|
|
174
|
+
else
|
|
175
|
+
task_name = current_logs[0][:task_name]
|
|
176
|
+
puts(task_name == 'pause' ? 'On pause' : "Now working on: [#{task_name.cyan}]")
|
|
177
|
+
unless current_logs[1..-1].empty?
|
|
178
|
+
puts "next tasks: #{current_logs[1..-1].map { |l| l[:task_name].light_blue }.join(', ')}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def current
|
|
184
|
+
puts `cat #{@ftg_dir}/current.txt`
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def edit(day, restore, reset)
|
|
188
|
+
require_relative './interactive'
|
|
189
|
+
require_relative './ftg_stats'
|
|
190
|
+
require_models
|
|
191
|
+
|
|
192
|
+
ftg_stats = FtgStats.new(day == Time.now.strftime('%F'))
|
|
193
|
+
tasks = []
|
|
194
|
+
Hash[ftg_stats.stats][day].each do |branch, by_branch|
|
|
195
|
+
next if branch == 'unknown'
|
|
196
|
+
by_idle = Hash[by_branch]
|
|
197
|
+
scope = Task.where(day: day).where(name: branch)
|
|
198
|
+
scope.delete_all if reset
|
|
199
|
+
task = scope.first_or_create
|
|
200
|
+
task.duration = task.edited_at ? task.duration : by_idle[false].to_i
|
|
201
|
+
task.save
|
|
202
|
+
tasks << task if restore || !task.deleted_at
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
deleted_tasks = Interactive.new.interactive_edit(tasks)
|
|
206
|
+
tasks.each do |task|
|
|
207
|
+
task.deleted_at = nil if restore
|
|
208
|
+
task.save if restore || task.changed.include?('edited_at')
|
|
209
|
+
end
|
|
210
|
+
deleted_tasks.each do |task|
|
|
211
|
+
task.save if task.changed.include?('deleted_at')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def sync
|
|
216
|
+
require_relative './ftg_sync'
|
|
217
|
+
abort('todo')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def git_stats
|
|
221
|
+
require_relative './ftg_stats'
|
|
222
|
+
FtgStats.new(false).run
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def list(days)
|
|
226
|
+
days ||= 14
|
|
227
|
+
begin_time = Time.now.to_i - (days.to_i * 24 * 3600)
|
|
228
|
+
|
|
229
|
+
# Date.parse(day).to_time.to_i
|
|
230
|
+
git_branches_raw = `git for-each-ref --sort=-committerdate --format='%(refname:short) | %(committerdate:iso)' refs/heads/` rescue nil
|
|
231
|
+
|
|
232
|
+
git_branches = []
|
|
233
|
+
git_branches_raw.split("\n").map do |b|
|
|
234
|
+
parts = b.split(' | ')
|
|
235
|
+
next if parts.count != 2
|
|
236
|
+
timestamp = DateTime.parse(parts[1]).to_time.to_i
|
|
237
|
+
if timestamp > begin_time
|
|
238
|
+
git_branches << [timestamp, parts[0]]
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
commands_log_path = "#{@ftg_dir}/log/commands.log"
|
|
243
|
+
history_branches = []
|
|
244
|
+
`tail -n #{days * 500} #{commands_log_path}`.split("\n").each do |command|
|
|
245
|
+
parts = command.split("\t")
|
|
246
|
+
time = parts[5].to_i
|
|
247
|
+
branch = parts[4]
|
|
248
|
+
if time > begin_time && branch != 'no_branch'
|
|
249
|
+
history_branches << [time, branch]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
history_branches = history_branches.group_by { |e| e[1] }.map { |k, v| [v.last[0], k] }
|
|
253
|
+
|
|
254
|
+
ftg_log_path = "#{@ftg_dir}/log/ftg.log"
|
|
255
|
+
ftg_tasks = []
|
|
256
|
+
`tail -n #{days * 100} #{ftg_log_path}`.split("\n").each do |log|
|
|
257
|
+
parts = log.split("\t")
|
|
258
|
+
task = parts[1]
|
|
259
|
+
time = parts[2].to_i
|
|
260
|
+
if time > begin_time
|
|
261
|
+
ftg_tasks << [time, task]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
ftg_tasks = ftg_tasks.group_by { |e| e[1] }.map { |k, v| [v.last[0], k] }
|
|
265
|
+
|
|
266
|
+
all_tasks = git_branches + history_branches + ftg_tasks
|
|
267
|
+
all_tasks = all_tasks.sort_by { |e| -e[0] }.group_by { |e| e[1] }.map { |task, times| task }
|
|
268
|
+
puts all_tasks.join("\n")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def migrate
|
|
272
|
+
require_models
|
|
273
|
+
CreateTasks.new.up
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def render_email(day, tasks)
|
|
277
|
+
max_len = TaskFormatter.max_length(tasks)
|
|
278
|
+
content = "Salut,\n\n<Expliquer ici pourquoi le sprint ne sera pas fini à temps>\n\n#{day}\n"
|
|
279
|
+
content += tasks.map do |task|
|
|
280
|
+
TaskFormatter.new.format(task, max_len).line_for_email
|
|
281
|
+
end.join("\n")
|
|
282
|
+
content
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def email(day)
|
|
286
|
+
require_models
|
|
287
|
+
email = @config['ftg']['recap_mailto'].join(', ')
|
|
288
|
+
week_days_fr = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
|
|
289
|
+
week_day_fr = week_days_fr[Date.parse(day).strftime('%u').to_i - 1]
|
|
290
|
+
week_day_en = Time.now.strftime('%A').downcase
|
|
291
|
+
greeting = @config['ftg']['greetings'][week_day_en] || nil
|
|
292
|
+
subject = "Recap #{week_day_fr} #{day}"
|
|
293
|
+
|
|
294
|
+
body = [render_email(day, Task.where(day: day).where(deleted_at: nil)), greeting].compact.join("\n\n")
|
|
295
|
+
system('open', "mailto: #{email}?subject=#{subject}&body=#{body}")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def console
|
|
299
|
+
require 'pry'
|
|
300
|
+
require_relative './interactive'
|
|
301
|
+
require_relative './ftg_stats'
|
|
302
|
+
require_models
|
|
303
|
+
binding.pry
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def coffee(big = false)
|
|
307
|
+
require_relative './coffee'
|
|
308
|
+
puts(big ? Coffee.coffee2 : Coffee.coffee1)
|
|
309
|
+
puts "\nHave a nice coffee !"
|
|
310
|
+
puts '=========================================='
|
|
311
|
+
pause
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# Ftg.new.run
|
data/lib/ftg_logger.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class FtgLogger
|
|
2
|
+
|
|
3
|
+
def initialize(ftg_dir)
|
|
4
|
+
@ftg_dir = ftg_dir
|
|
5
|
+
@log_file = "#{ftg_dir}/log/ftg.log"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def add_log(command, task)
|
|
9
|
+
lines = [command, task, Time.now.getutc.to_i]
|
|
10
|
+
`echo "#{lines.join('\t')}" >> #{@log_file}`
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def remove_all_logs
|
|
14
|
+
`echo "" > #{@log_file}`
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def remove_logs(name)
|
|
18
|
+
count = 0
|
|
19
|
+
logs = get_logs
|
|
20
|
+
logs.keep_if do |log|
|
|
21
|
+
cond = log[:task_name] != name || log[:timestamp].to_i <= Time.now.to_i - 24*3600
|
|
22
|
+
count += 1 unless cond
|
|
23
|
+
cond
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
File.open(@log_file, 'w') do |f|
|
|
27
|
+
f.write(logs.map{|l| l.values.join("\t")}.join("\n") + "\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Removed #{count} entries"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_logs
|
|
34
|
+
File.open(@log_file, File::RDONLY|File::CREAT) do |file|
|
|
35
|
+
file.read.split("\n").map do |e|
|
|
36
|
+
parts = e.split("\t")
|
|
37
|
+
{ command: parts[0], task_name: parts[1], timestamp: parts[2] }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_pause?
|
|
43
|
+
unclosed_logs = get_unclosed_logs
|
|
44
|
+
unclosed_logs[0] && unclosed_logs[0][:task_name] == 'pause'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_unclosed_logs
|
|
48
|
+
unclosed_logs = []
|
|
49
|
+
closed = {}
|
|
50
|
+
get_logs.reverse.each do |log|
|
|
51
|
+
if log[:command] == 'ftg_stop'
|
|
52
|
+
closed[log[:task_name]] = true
|
|
53
|
+
end
|
|
54
|
+
if log[:command] == 'ftg_start' && !closed[log[:task_name]]
|
|
55
|
+
unclosed_logs << log
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
unclosed_logs
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update_current
|
|
62
|
+
current = ''
|
|
63
|
+
current_logs = get_unclosed_logs
|
|
64
|
+
current = current_logs[0][:task_name] unless current_logs.empty?
|
|
65
|
+
`echo "#{current}" > #{@ftg_dir}/current.txt`
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/ftg_options.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module FtgOptions
|
|
2
|
+
def get_option(names)
|
|
3
|
+
ARGV.each_with_index do |opt_name, i|
|
|
4
|
+
return (ARGV[i + 1] || 1) if names.include?(opt_name)
|
|
5
|
+
end
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# day, not gay
|
|
10
|
+
def day_option
|
|
11
|
+
day_option = get_option(['-d', '--day'])
|
|
12
|
+
day_option ||= '0'
|
|
13
|
+
|
|
14
|
+
Utils.is_integer?(day_option) ?
|
|
15
|
+
Time.at(Time.now.to_i - day_option.to_i * 86400).strftime('%F') :
|
|
16
|
+
Date.parse(day_option).strftime('%F')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_command(name)
|
|
20
|
+
@commands.find { |cmd_name, _| cmd_name.to_s.start_with?(name) } ||
|
|
21
|
+
@commands.find { |_, cmd| cmd[:aliases] && cmd[:aliases].any? { |a| a.to_s.start_with?(name) } }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fail(message = nil)
|
|
25
|
+
STDERR.puts message if message
|
|
26
|
+
exit(1)
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/ftg_stats.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
class FtgStats
|
|
2
|
+
IDLE_THRESHOLD = 5 * 60
|
|
3
|
+
|
|
4
|
+
attr_accessor :stats
|
|
5
|
+
|
|
6
|
+
def initialize(only_last_day)
|
|
7
|
+
load_data(only_last_day)
|
|
8
|
+
crunch
|
|
9
|
+
group
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
|
|
14
|
+
display
|
|
15
|
+
# sync_toggl
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def search_idle_key(timestamp)
|
|
19
|
+
(0..10).each do |k|
|
|
20
|
+
key = timestamp + k
|
|
21
|
+
return key if @idle_parts[key]
|
|
22
|
+
end
|
|
23
|
+
# puts("not found #{Utils.format_time(timestamp)}")
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_data(only_last_day)
|
|
28
|
+
home = `echo $HOME`.strip
|
|
29
|
+
ftg_dir = "#{home}/.ftg"
|
|
30
|
+
commands_log_path = "#{ftg_dir}/log/commands.log"
|
|
31
|
+
idle_log_path = "#{ftg_dir}/log/idle.log"
|
|
32
|
+
records_to_load = only_last_day ? 24 * 360 : 0
|
|
33
|
+
@commands = {}
|
|
34
|
+
@idle_parts = {}
|
|
35
|
+
|
|
36
|
+
# sample row:
|
|
37
|
+
# pinouchon fg no_alias /Users/pinouchon/.ftg no_branch 1438867098
|
|
38
|
+
(only_last_day ?
|
|
39
|
+
`tail -n #{records_to_load} #{commands_log_path}`.split("\n") :
|
|
40
|
+
File.foreach(commands_log_path)).each do |line|
|
|
41
|
+
parts = line.split("\t")
|
|
42
|
+
next if !parts[5] || parts[5].empty?
|
|
43
|
+
@commands[parts[5].strip.to_i] = { :user => parts[0],
|
|
44
|
+
:command => parts[1],
|
|
45
|
+
:alias => parts[2], :dir => parts[3], :branch => parts[4] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
(only_last_day ?
|
|
49
|
+
`tail -n #{records_to_load} #{idle_log_path}`.split("\n") :
|
|
50
|
+
File.foreach(idle_log_path)).each do |line|
|
|
51
|
+
parts = line.split("\t")
|
|
52
|
+
next if !parts[1] || parts[1].empty?
|
|
53
|
+
@idle_parts[parts[1].strip.to_i] = { :time_elapsed => parts[0] }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def crunch
|
|
58
|
+
# tagging branches in idle_parts
|
|
59
|
+
@commands.each do |timestamp, command_info|
|
|
60
|
+
if (key = search_idle_key(timestamp))
|
|
61
|
+
@idle_parts[key][:branch] = command_info[:branch]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# filling branches in idle_parts
|
|
66
|
+
# tagging thresholds in idle_parts
|
|
67
|
+
last_branch = 'unknown'
|
|
68
|
+
@idle_parts.each do |timestamp, part|
|
|
69
|
+
if part[:branch] && part[:branch] != '' && part[:branch] != 'no_branch'
|
|
70
|
+
last_branch = part[:branch]
|
|
71
|
+
end
|
|
72
|
+
# puts "setting to #{last_branch} (#{Time.at(timestamp).strftime('%Y/%m/%d at %I:%M%p')})"
|
|
73
|
+
@idle_parts[timestamp][:branch] = last_branch
|
|
74
|
+
@idle_parts[timestamp][:idle] = part[:time_elapsed].to_i > IDLE_THRESHOLD
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def group
|
|
79
|
+
@stats = @idle_parts.group_by { |ts, _| Time.at(ts).strftime('%F') }.map do |day, parts_by_day|
|
|
80
|
+
[
|
|
81
|
+
day,
|
|
82
|
+
parts_by_day.group_by { |_, v| v[:branch] }.map do |branch, parts_by_branch|
|
|
83
|
+
[
|
|
84
|
+
branch,
|
|
85
|
+
parts_by_branch.group_by { |_, v| v[:idle] }.map { |k, v| [k, v.count*10] }
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def display
|
|
93
|
+
Hash[@stats].each do |day, by_day|
|
|
94
|
+
puts "#{day}:"
|
|
95
|
+
Hash[by_day].each do |branch, by_branch|
|
|
96
|
+
by_idle = Hash[by_branch]
|
|
97
|
+
idle_str = by_idle[true] ? "(and #{Utils.format_time(by_idle[true])} idle)" : ''
|
|
98
|
+
puts " #{branch}: #{Utils.format_time(by_idle[false]) || '00:00:00'} #{idle_str}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def sync_toggl
|
|
104
|
+
require 'pry'
|
|
105
|
+
sync = FtgSync.new
|
|
106
|
+
i = 0
|
|
107
|
+
|
|
108
|
+
Hash[@stats].each do |day, by_day|
|
|
109
|
+
puts "#{day}:"
|
|
110
|
+
Hash[by_day].each do |branch, by_branch|
|
|
111
|
+
by_idle = Hash[by_branch]
|
|
112
|
+
idle_str = by_idle[true] ? "(and #{by_idle[true]} idle)" : ''
|
|
113
|
+
puts " #{branch}: #{by_idle[false] || '00:00:00'} #{idle_str}"
|
|
114
|
+
|
|
115
|
+
if branch =~ /jt-/ && by_idle[false]
|
|
116
|
+
ps = day.split('-')
|
|
117
|
+
time = Time.new(ps[0], ps[1], ps[2], 12,0,0)
|
|
118
|
+
begining_of_day = Time.new(ps[0], ps[1], ps[2], 0,0,0)
|
|
119
|
+
end_of_day = begining_of_day + (24*3600)
|
|
120
|
+
|
|
121
|
+
jt = branch[/(jt-[0-9]+)/]
|
|
122
|
+
duration_parts = by_idle[false].split(':')
|
|
123
|
+
duration = duration_parts[0].to_i * 3600 + duration_parts[1].to_i * 60 + duration_parts[2].to_i
|
|
124
|
+
type = sync.maintenance?(jt) ? :maintenance : :sprint
|
|
125
|
+
sync.create_entry("#{branch} [via FTG]", duration, time, type)
|
|
126
|
+
i += 1
|
|
127
|
+
|
|
128
|
+
puts "logging #{branch}: #{by_idle[false]}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
puts "total: #{i}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
end
|