airbrake-ruby 3.2.2-java
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/lib/airbrake-ruby.rb +554 -0
- data/lib/airbrake-ruby/async_sender.rb +119 -0
- data/lib/airbrake-ruby/backtrace.rb +194 -0
- data/lib/airbrake-ruby/code_hunk.rb +53 -0
- data/lib/airbrake-ruby/config.rb +238 -0
- data/lib/airbrake-ruby/config/validator.rb +63 -0
- data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
- data/lib/airbrake-ruby/file_cache.rb +48 -0
- data/lib/airbrake-ruby/filter_chain.rb +95 -0
- data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
- data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
- data/lib/airbrake-ruby/hash_keyable.rb +37 -0
- data/lib/airbrake-ruby/ignorable.rb +44 -0
- data/lib/airbrake-ruby/nested_exception.rb +39 -0
- data/lib/airbrake-ruby/notice.rb +165 -0
- data/lib/airbrake-ruby/notice_notifier.rb +228 -0
- data/lib/airbrake-ruby/performance_notifier.rb +161 -0
- data/lib/airbrake-ruby/promise.rb +99 -0
- data/lib/airbrake-ruby/response.rb +71 -0
- data/lib/airbrake-ruby/stat.rb +56 -0
- data/lib/airbrake-ruby/sync_sender.rb +111 -0
- data/lib/airbrake-ruby/tdigest.rb +393 -0
- data/lib/airbrake-ruby/time_truncate.rb +17 -0
- data/lib/airbrake-ruby/truncator.rb +115 -0
- data/lib/airbrake-ruby/version.rb +6 -0
- data/spec/airbrake_spec.rb +171 -0
- data/spec/async_sender_spec.rb +154 -0
- data/spec/backtrace_spec.rb +438 -0
- data/spec/code_hunk_spec.rb +118 -0
- data/spec/config/validator_spec.rb +189 -0
- data/spec/config_spec.rb +281 -0
- data/spec/deploy_notifier_spec.rb +41 -0
- data/spec/file_cache.rb +36 -0
- data/spec/filter_chain_spec.rb +83 -0
- data/spec/filters/context_filter_spec.rb +25 -0
- data/spec/filters/dependency_filter_spec.rb +14 -0
- data/spec/filters/exception_attributes_filter_spec.rb +63 -0
- data/spec/filters/gem_root_filter_spec.rb +44 -0
- data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
- data/spec/filters/git_repository_filter.rb +53 -0
- data/spec/filters/git_revision_filter_spec.rb +126 -0
- data/spec/filters/keys_blacklist_spec.rb +236 -0
- data/spec/filters/keys_whitelist_spec.rb +205 -0
- data/spec/filters/root_directory_filter_spec.rb +42 -0
- data/spec/filters/sql_filter_spec.rb +219 -0
- data/spec/filters/system_exit_filter_spec.rb +14 -0
- data/spec/filters/thread_filter_spec.rb +279 -0
- data/spec/fixtures/notroot.txt +7 -0
- data/spec/fixtures/project_root/code.rb +221 -0
- data/spec/fixtures/project_root/empty_file.rb +0 -0
- data/spec/fixtures/project_root/long_line.txt +1 -0
- data/spec/fixtures/project_root/short_file.rb +3 -0
- data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
- data/spec/helpers.rb +9 -0
- data/spec/ignorable_spec.rb +14 -0
- data/spec/nested_exception_spec.rb +75 -0
- data/spec/notice_notifier_spec.rb +436 -0
- data/spec/notice_notifier_spec/options_spec.rb +266 -0
- data/spec/notice_spec.rb +297 -0
- data/spec/performance_notifier_spec.rb +287 -0
- data/spec/promise_spec.rb +165 -0
- data/spec/response_spec.rb +82 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/stat_spec.rb +35 -0
- data/spec/sync_sender_spec.rb +140 -0
- data/spec/tdigest_spec.rb +230 -0
- data/spec/time_truncate_spec.rb +13 -0
- data/spec/truncator_spec.rb +238 -0
- metadata +278 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Airbrake
|
2
|
+
class Config
|
3
|
+
# Validates values of {Airbrake::Config} options.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
# @since v1.5.0
|
7
|
+
class Validator
|
8
|
+
# @return [String]
|
9
|
+
REQUIRED_KEY_MSG = ':project_key is required'.freeze
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
REQUIRED_ID_MSG = ':project_id is required'.freeze
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
WRONG_ENV_TYPE_MSG = "the 'environment' option must be configured " \
|
16
|
+
"with a Symbol (or String), but '%s' was provided: " \
|
17
|
+
'%s'.freeze
|
18
|
+
|
19
|
+
# @return [Array<Class>] the list of allowed types to configure the
|
20
|
+
# environment option
|
21
|
+
VALID_ENV_TYPES = [NilClass, String, Symbol].freeze
|
22
|
+
|
23
|
+
# @return [String] error message, if validator was able to find any errors
|
24
|
+
# in the config
|
25
|
+
attr_reader :error_message
|
26
|
+
|
27
|
+
# Validates given config and stores error message, if any errors were
|
28
|
+
# found.
|
29
|
+
#
|
30
|
+
# @param config [Airbrake::Config] the config to validate
|
31
|
+
def initialize(config)
|
32
|
+
@config = config
|
33
|
+
@error_message = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
def valid_project_id?
|
38
|
+
valid = @config.project_id.to_i > 0
|
39
|
+
@error_message = REQUIRED_ID_MSG unless valid
|
40
|
+
valid
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean]
|
44
|
+
def valid_project_key?
|
45
|
+
valid = @config.project_key.is_a?(String) && !@config.project_key.empty?
|
46
|
+
@error_message = REQUIRED_KEY_MSG unless valid
|
47
|
+
valid
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Boolean]
|
51
|
+
def valid_environment?
|
52
|
+
environment = @config.environment
|
53
|
+
valid = VALID_ENV_TYPES.any? { |type| environment.is_a?(type) }
|
54
|
+
|
55
|
+
unless valid
|
56
|
+
@error_message = format(WRONG_ENV_TYPE_MSG, environment.class, environment)
|
57
|
+
end
|
58
|
+
|
59
|
+
valid
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Airbrake
|
2
|
+
# DeployNotifier sends deploy information to Airbrake. The information
|
3
|
+
# consists of:
|
4
|
+
# - environment
|
5
|
+
# - username
|
6
|
+
# - repository
|
7
|
+
# - revision
|
8
|
+
# - version
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
# @since v3.2.0
|
12
|
+
class DeployNotifier
|
13
|
+
# @param [Airbrake::Config] config
|
14
|
+
def initialize(config)
|
15
|
+
@config =
|
16
|
+
if config.is_a?(Config)
|
17
|
+
config
|
18
|
+
else
|
19
|
+
loc = caller_locations(1..1).first
|
20
|
+
signature = "#{self.class.name}##{__method__}"
|
21
|
+
warn(
|
22
|
+
"#{loc.path}:#{loc.lineno}: warning: passing a Hash to #{signature} " \
|
23
|
+
'is deprecated. Pass `Airbrake::Config` instead'
|
24
|
+
)
|
25
|
+
Config.new(config)
|
26
|
+
end
|
27
|
+
|
28
|
+
@sender = SyncSender.new(@config)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see Airbrake.create_deploy
|
32
|
+
def notify(deploy_info, promise = Airbrake::Promise.new)
|
33
|
+
if @config.ignored_environment?
|
34
|
+
return promise.reject("The '#{@config.environment}' environment is ignored")
|
35
|
+
end
|
36
|
+
|
37
|
+
deploy_info[:environment] ||= @config.environment
|
38
|
+
@sender.send(
|
39
|
+
deploy_info,
|
40
|
+
promise,
|
41
|
+
URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys")
|
42
|
+
)
|
43
|
+
|
44
|
+
promise
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Airbrake
|
2
|
+
# Extremely simple global cache.
|
3
|
+
#
|
4
|
+
# @api private
|
5
|
+
# @since v2.4.1
|
6
|
+
module FileCache
|
7
|
+
# @return [Integer]
|
8
|
+
MAX_SIZE = 50
|
9
|
+
|
10
|
+
# @return [Mutex]
|
11
|
+
MUTEX = Mutex.new
|
12
|
+
|
13
|
+
# Associates the value given by +value+ with the key given by +key+. Deletes
|
14
|
+
# entries that exceed +MAX_SIZE+.
|
15
|
+
#
|
16
|
+
# @param [Object] key
|
17
|
+
# @param [Object] value
|
18
|
+
# @return [Object] the corresponding value
|
19
|
+
def self.[]=(key, value)
|
20
|
+
MUTEX.synchronize do
|
21
|
+
data[key] = value
|
22
|
+
data.delete(data.keys.first) if data.size > MAX_SIZE
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Retrieve an object from the cache.
|
27
|
+
#
|
28
|
+
# @param [Object] key
|
29
|
+
# @return [Object] the corresponding value
|
30
|
+
def self.[](key)
|
31
|
+
MUTEX.synchronize do
|
32
|
+
data[key]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks whether the cache is empty. Needed only for the test suite.
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
def self.empty?
|
40
|
+
data.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.data
|
44
|
+
@data ||= {}
|
45
|
+
end
|
46
|
+
private_class_method :data
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Airbrake
|
2
|
+
# FilterChain represents an ordered array of filters.
|
3
|
+
#
|
4
|
+
# A filter is an object that responds to <b>#call</b> (typically a Proc or a
|
5
|
+
# class that implements the call method). The <b>#call</b> method must accept
|
6
|
+
# exactly one argument: an object to be filtered.
|
7
|
+
#
|
8
|
+
# When you add a new filter to the chain, it gets inserted according to its
|
9
|
+
# <b>weight</b>. Smaller weight means the filter will be somewhere in the
|
10
|
+
# beginning of the array. Larger - in the end. If a filter doesn't implement
|
11
|
+
# weight, the chain assumes it's equal to 0.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class MyFilter
|
15
|
+
# attr_reader :weight
|
16
|
+
#
|
17
|
+
# def initialize
|
18
|
+
# @weight = 1
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def call(obj)
|
22
|
+
# puts 'Filtering...'
|
23
|
+
# obj[:data] = '[Filtered]'
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# filter_chain = FilterChain.new
|
28
|
+
# filter_chain.add_filter(MyFilter)
|
29
|
+
#
|
30
|
+
# filter_chain.refine(obj)
|
31
|
+
# #=> Filtering...
|
32
|
+
#
|
33
|
+
# @see Airbrake.add_filter
|
34
|
+
# @api private
|
35
|
+
# @since v1.0.0
|
36
|
+
class FilterChain
|
37
|
+
# @return [Integer]
|
38
|
+
DEFAULT_WEIGHT = 0
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@filters = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# Adds a filter to the filter chain. Sorts filters by weight.
|
45
|
+
#
|
46
|
+
# @param [#call] filter The filter object (proc, class, module, etc)
|
47
|
+
# @return [void]
|
48
|
+
def add_filter(filter)
|
49
|
+
@filters = (@filters << filter).sort_by do |f|
|
50
|
+
f.respond_to?(:weight) ? f.weight : DEFAULT_WEIGHT
|
51
|
+
end.reverse!
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deletes a filter from the the filter chain.
|
55
|
+
#
|
56
|
+
# @param [Class] filter_class The class of the filter you want to delete
|
57
|
+
# @return [void]
|
58
|
+
# @since v3.1.0
|
59
|
+
def delete_filter(filter_class)
|
60
|
+
index = @filters.index { |f| f.class.name == filter_class.name }
|
61
|
+
@filters.delete_at(index) if index
|
62
|
+
end
|
63
|
+
|
64
|
+
# Applies all the filters in the filter chain to the given notice. Does not
|
65
|
+
# filter ignored notices.
|
66
|
+
#
|
67
|
+
# @param [Airbrake::Notice] notice The notice to be filtered
|
68
|
+
# @return [void]
|
69
|
+
# @todo Make it work with anything, not only notices
|
70
|
+
def refine(notice)
|
71
|
+
@filters.each do |filter|
|
72
|
+
break if notice.ignored?
|
73
|
+
filter.call(notice)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [String] customized inspect to lessen the amount of clutter
|
78
|
+
def inspect
|
79
|
+
@filters.map(&:class)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String] {#inspect} for PrettyPrint
|
83
|
+
def pretty_print(q)
|
84
|
+
q.text('[')
|
85
|
+
|
86
|
+
# Make nesting of the first element consistent on JRuby and MRI.
|
87
|
+
q.nest(2) { q.breakable }
|
88
|
+
|
89
|
+
q.nest(2) do
|
90
|
+
q.seplist(@filters) { |f| q.pp(f.class) }
|
91
|
+
end
|
92
|
+
q.text(']')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Airbrake
|
2
|
+
module Filters
|
3
|
+
# Adds user context to the notice object. Clears the context after it's
|
4
|
+
# attached.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @since v2.9.0
|
8
|
+
class ContextFilter
|
9
|
+
# @return [Integer]
|
10
|
+
attr_reader :weight
|
11
|
+
|
12
|
+
def initialize(context)
|
13
|
+
@context = context
|
14
|
+
@weight = 119
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# @macro call_filter
|
19
|
+
def call(notice)
|
20
|
+
@mutex.synchronize do
|
21
|
+
return if @context.empty?
|
22
|
+
|
23
|
+
notice[:params][:airbrake_context] = @context.dup
|
24
|
+
@context.clear
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Airbrake
|
2
|
+
module Filters
|
3
|
+
# Attaches loaded dependencies to the notice object.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
# @since v2.10.0
|
7
|
+
class DependencyFilter
|
8
|
+
def initialize
|
9
|
+
@weight = 117
|
10
|
+
end
|
11
|
+
|
12
|
+
# @macro call_filter
|
13
|
+
def call(notice)
|
14
|
+
deps = {}
|
15
|
+
Gem.loaded_specs.map.with_object(deps) do |(name, spec), h|
|
16
|
+
h[name] = "#{spec.version}#{git_version(spec)}"
|
17
|
+
end
|
18
|
+
|
19
|
+
notice[:context][:versions] = {} unless notice[:context].key?(:versions)
|
20
|
+
notice[:context][:versions][:dependencies] = deps
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def git_version(spec)
|
26
|
+
return unless spec.respond_to?(:git_version) || spec.git_version
|
27
|
+
spec.git_version.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Airbrake
|
2
|
+
module Filters
|
3
|
+
# ExceptionAttributesFilter attempts to call `#to_airbrake` on the stashed
|
4
|
+
# exception and attaches returned data to the notice object.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @since v2.10.0
|
8
|
+
class ExceptionAttributesFilter
|
9
|
+
def initialize(logger)
|
10
|
+
@logger = logger
|
11
|
+
@weight = 118
|
12
|
+
end
|
13
|
+
|
14
|
+
# @macro call_filter
|
15
|
+
def call(notice)
|
16
|
+
exception = notice.stash[:exception]
|
17
|
+
return unless exception.respond_to?(:to_airbrake)
|
18
|
+
|
19
|
+
attributes = nil
|
20
|
+
begin
|
21
|
+
attributes = exception.to_airbrake
|
22
|
+
rescue StandardError => ex
|
23
|
+
@logger.error(
|
24
|
+
"#{LOG_LABEL} #{exception.class}#to_airbrake failed. #{ex.class}: #{ex}"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
unless attributes.is_a?(Hash)
|
29
|
+
@logger.error(
|
30
|
+
"#{LOG_LABEL} #{self.class}: wanted Hash, got #{attributes.class}"
|
31
|
+
)
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
attributes.each do |key, attrs|
|
36
|
+
if notice[key]
|
37
|
+
notice[key].merge!(attrs)
|
38
|
+
else
|
39
|
+
notice[key] = attrs
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Airbrake
|
2
|
+
module Filters
|
3
|
+
# Replaces paths to gems with a placeholder.
|
4
|
+
# @api private
|
5
|
+
class GemRootFilter
|
6
|
+
# @return [String]
|
7
|
+
GEM_ROOT_LABEL = '/GEM_ROOT'.freeze
|
8
|
+
|
9
|
+
# @return [Integer]
|
10
|
+
attr_reader :weight
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@weight = 120
|
14
|
+
end
|
15
|
+
|
16
|
+
# @macro call_filter
|
17
|
+
def call(notice)
|
18
|
+
return unless defined?(Gem)
|
19
|
+
|
20
|
+
notice[:errors].each do |error|
|
21
|
+
Gem.path.each do |gem_path|
|
22
|
+
error[:backtrace].each do |frame|
|
23
|
+
# If the frame is unparseable, then 'file' is nil, thus nothing to
|
24
|
+
# filter (all frame's data is in 'function' instead).
|
25
|
+
next unless (file = frame[:file])
|
26
|
+
frame[:file] = file.sub(/\A#{gem_path}/, GEM_ROOT_LABEL)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Airbrake
|
4
|
+
module Filters
|
5
|
+
# Attaches git checkout info to `context`. The info includes:
|
6
|
+
# * username
|
7
|
+
# * email
|
8
|
+
# * revision
|
9
|
+
# * time
|
10
|
+
#
|
11
|
+
# This information is used to track deploys automatically.
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
# @since v2.12.0
|
15
|
+
class GitLastCheckoutFilter
|
16
|
+
# @return [Integer]
|
17
|
+
attr_reader :weight
|
18
|
+
|
19
|
+
# @return [Integer] least possible amount of columns in git's `logs/HEAD`
|
20
|
+
# file (checkout information is omitted)
|
21
|
+
MIN_HEAD_COLS = 6
|
22
|
+
|
23
|
+
# @param [Logger] logger
|
24
|
+
# @param [String] root_directory
|
25
|
+
def initialize(logger, root_directory)
|
26
|
+
@git_path = File.join(root_directory, '.git')
|
27
|
+
@logger = logger
|
28
|
+
@weight = 116
|
29
|
+
@last_checkout = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# @macro call_filter
|
33
|
+
def call(notice)
|
34
|
+
return if notice[:context].key?(:lastCheckout)
|
35
|
+
|
36
|
+
if @last_checkout
|
37
|
+
notice[:context][:lastCheckout] = @last_checkout
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
return unless File.exist?(@git_path)
|
42
|
+
return unless (checkout = last_checkout)
|
43
|
+
notice[:context][:lastCheckout] = checkout
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def last_checkout
|
49
|
+
return unless (line = last_checkout_line)
|
50
|
+
|
51
|
+
parts = line.chomp.split("\t").first.split(' ')
|
52
|
+
if parts.size < MIN_HEAD_COLS
|
53
|
+
@logger.error(
|
54
|
+
"#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}"
|
55
|
+
)
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
author = parts[2..-4]
|
60
|
+
@last_checkout = {
|
61
|
+
username: author[0..1].join(' '),
|
62
|
+
email: parts[-3][1..-2],
|
63
|
+
revision: parts[1],
|
64
|
+
time: timestamp(parts[-2].to_i)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def last_checkout_line
|
69
|
+
head_path = File.join(@git_path, 'logs', 'HEAD')
|
70
|
+
return unless File.exist?(head_path)
|
71
|
+
|
72
|
+
last_line = nil
|
73
|
+
IO.foreach(head_path) do |line|
|
74
|
+
last_line = line if checkout_line?(line)
|
75
|
+
end
|
76
|
+
last_line
|
77
|
+
end
|
78
|
+
|
79
|
+
def checkout_line?(line)
|
80
|
+
line.include?("\tclone:") ||
|
81
|
+
line.include?("\tpull:") ||
|
82
|
+
line.include?("\tcheckout:")
|
83
|
+
end
|
84
|
+
|
85
|
+
def timestamp(utime)
|
86
|
+
Time.at(utime).to_datetime.rfc3339
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|