Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

History refactors #1013

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

require_relative "irb/ruby-lex"
require_relative "irb/statement"
require_relative "irb/history"
require_relative "irb/input-method"
require_relative "irb/locale"
require_relative "irb/color"
Expand Down Expand Up @@ -972,7 +973,7 @@ def debug_readline(binding)
# debugger.
input = nil
forced_exit = catch(:IRB_EXIT) do
if IRB.conf[:SAVE_HISTORY] && context.io.support_history_saving?
if History.save_history? && context.io.support_history_saving?
# Previous IRB session's history has been saved when `Irb#run` is exited We need
# to make sure the saved history is not saved again by resetting the counter
context.io.reset_history_counter
Expand Down Expand Up @@ -1003,9 +1004,10 @@ def run(conf = IRB.conf)
prev_context = conf[:MAIN_CONTEXT]
conf[:MAIN_CONTEXT] = context

save_history = !in_nested_session && conf[:SAVE_HISTORY] && context.io.support_history_saving?
load_history = !in_nested_session && context.io.support_history_saving?
save_history = load_history && History.save_history?

if save_history
if load_history
context.io.load_history
end

Expand Down
144 changes: 89 additions & 55 deletions lib/irb/history.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
require "pathname"

module IRB
module History
class << self
# Integer representation of <code>IRB.conf[:HISTORY_FILE]</code>.
def save_history
num = IRB.conf[:SAVE_HISTORY].to_i
# Bignums cause RangeErrors when slicing arrays.
# Treat such values as 'infinite'.
(num > save_history_max) ? -1 : num
end

def save_history?
!save_history.zero?
end

def infinite?
save_history.negative?
end

# Might be nil when HOME and XDG_CONFIG_HOME are not available.
def history_file
if (history_file = IRB.conf[:HISTORY_FILE])
File.expand_path(history_file)
else
IRB.rc_file("_history")
end
end

private

def save_history_max
# Max fixnum (32-bit) that can be used without getting RangeError.
2**30 - 1
end
end
end

module HistorySavingAbility # :nodoc:
def support_history_saving?
true
Expand All @@ -11,76 +47,74 @@ def reset_history_counter
end

def load_history
history_file = History.history_file
return unless File.exist?(history_file.to_s)

history = self.class::HISTORY

if history_file = IRB.conf[:HISTORY_FILE]
history_file = File.expand_path(history_file)
end
history_file = IRB.rc_file("_history") unless history_file
if history_file && File.exist?(history_file)
File.open(history_file, "r:#{IRB.conf[:LC_MESSAGES].encoding}") do |f|
f.each { |l|
l = l.chomp
if self.class == RelineInputMethod and history.last&.end_with?("\\")
history.last.delete_suffix!("\\")
history.last << "\n" << l
else
history << l
end
}
end
@loaded_history_lines = history.size
@loaded_history_mtime = File.mtime(history_file)
File.open(history_file, "r:#{IRB.conf[:LC_MESSAGES].encoding}") do |f|
f.each { |l|
l = l.chomp
if self.class == RelineInputMethod and history.last&.end_with?("\\")
history.last.delete_suffix!("\\")
history.last << "\n" << l
else
history << l
end
}
end
@loaded_history_lines = history.size
@loaded_history_mtime = File.mtime(history_file)
end

def save_history
return unless History.save_history?
return unless (history_file = History.history_file)
unless ensure_history_file_writable(history_file)
warn <<~WARN
Can't write history to #{History.history_file.inspect} due to insufficient permissions.
Please verify the value of `IRB.conf[:HISTORY_FILE]`. Ensure the folder exists and that both the folder and file (if it exists) are writable.
WARN
return
end

history = self.class::HISTORY.to_a

if num = IRB.conf[:SAVE_HISTORY] and (num = num.to_i) != 0
if history_file = IRB.conf[:HISTORY_FILE]
history_file = File.expand_path(history_file)
end
history_file = IRB.rc_file("_history") unless history_file
if File.exist?(history_file) &&
File.mtime(history_file) != @loaded_history_mtime
history = history[@loaded_history_lines..-1] if @loaded_history_lines
append_history = true
end

# When HOME and XDG_CONFIG_HOME are not available, history_file might be nil
return unless history_file
File.open(history_file, (append_history ? "a" : "w"), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f|
hist = history.map { |l| l.scrub.split("\n").join("\\\n") }

# Change the permission of a file that already exists[BUG #7694]
begin
if File.stat(history_file).mode & 066 != 0
File.chmod(0600, history_file)
end
rescue Errno::ENOENT
rescue Errno::EPERM
return
rescue
raise
unless append_history || History.infinite?
hist = hist.last(History.save_history)
end

if File.exist?(history_file) &&
File.mtime(history_file) != @loaded_history_mtime
history = history[@loaded_history_lines..-1] if @loaded_history_lines
append_history = true
end
f.puts(hist)
end
end

pathname = Pathname.new(history_file)
unless Dir.exist?(pathname.dirname)
warn "Warning: The directory to save IRB's history file does not exist. Please double check `IRB.conf[:HISTORY_FILE]`'s value."
return
end
private

File.open(history_file, (append_history ? 'a' : 'w'), 0o600, encoding: IRB.conf[:LC_MESSAGES]&.encoding) do |f|
hist = history.map{ |l| l.scrub.split("\n").join("\\\n") }
unless append_history
begin
hist = hist.last(num) if hist.size > num and num > 0
rescue RangeError # bignum too big to convert into `long'
# Do nothing because the bignum should be treated as infinity
end
end
f.puts(hist)
# Returns boolean whether writing to +history_file+ will be possible.
# Permissions of already existing +history_file+ are changed to
# owner-only-readable if necessary [BUG #7694].
def ensure_history_file_writable(history_file)
history_file = Pathname.new(history_file)

return false unless history_file.dirname.writable?
return true unless history_file.exist?

begin
if history_file.stat.mode & 0o66 != 0
history_file.chmod 0o600
end
true
rescue Errno::EPERM # no permissions
false
end
end
end
Expand Down
17 changes: 16 additions & 1 deletion test/irb/test_history.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ class TestInputMethodWithReadlineHistory < TestInputMethod
include IRB::HistorySavingAbility
end

def test_history_dont_save
omit "Skip Editline" if /EditLine/n.match(Readline::VERSION)
IRB.conf[:SAVE_HISTORY] = nil
assert_history(<<~EXPECTED_HISTORY, <<~INITIAL_HISTORY, <<~INPUT)
1
2
EXPECTED_HISTORY
1
2
INITIAL_HISTORY
3
exit
INPUT
end

def test_history_save_1
omit "Skip Editline" if /EditLine/n.match(Readline::VERSION)
IRB.conf[:SAVE_HISTORY] = 1
Expand Down Expand Up @@ -166,7 +181,7 @@ def test_history_does_not_raise_when_history_file_directory_does_not_exist
IRB.conf[:HISTORY_FILE] = "fake/fake/fake/history_file"
io = TestInputMethodWithRelineHistory.new

assert_warn(/history file does not exist/) do
assert_warn(/ensure the folder exists/i) do
io.save_history
end

Expand Down
Loading