Skip to content
Open
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
50 changes: 50 additions & 0 deletions lib/gitsh/commands/pipeline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require 'gitsh/pipeline_environment'

module Gitsh
module Commands
class Pipeline
def initialize(left, right)
@left = left
@right = right
end

def execute(env)
left_env, right_env = PipelineEnvironment.build_pair(env)
threads = [start_left_thread(left_env), start_right_thread(right_env)]
wait_for_threads(threads)
threads.map(&:value).all?
end

private

attr_reader :left, :right, :threads

def start_left_thread(env)
Thread.new { execute_left(env) }
end

def start_right_thread(env)
Thread.new { execute_right(env) }
end

def execute_left(left_env)
left.execute(left_env)
ensure
left_env.output_stream.close
end

def execute_right(right_env)
right.execute(right_env)
ensure
right_env.input_stream.close
end

def wait_for_threads(threads)
threads.map(&:join)
rescue Interrupt
threads.each { |thread| thread.raise(Interrupt) }
retry
end
end
end
end
1 change: 1 addition & 0 deletions lib/gitsh/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def initialize(*args)
rule(/\s*;\s*/) { :SEMICOLON }
rule(/\s*&&\s*/) { :AND }
rule(/\s*\|\|\s*/) { :OR }
rule(/\s*\|\s*/) { :PIPE }

[:default, :soft_string].each do |state|
rule(/\$\(\s*/, state) do
Expand Down
2 changes: 2 additions & 0 deletions lib/gitsh/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'gitsh/commands/shell_command'
require 'gitsh/commands/noop'
require 'gitsh/commands/tree'
require 'gitsh/commands/pipeline'

module Gitsh
class Parser < RLTK::Parser
Expand Down Expand Up @@ -36,6 +37,7 @@ class Parser < RLTK::Parser
clause('.commands SEMICOLON .commands') { |c1, c2| Commands::Tree::Multi.new(c1, c2) }
clause('.commands OR .commands') { |c1, c2| Commands::Tree::Or.new(c1, c2) }
clause('.commands AND .commands') { |c1, c2| Commands::Tree::And.new(c1, c2) }
clause('.commands PIPE .commands') { |c1, c2| Commands::Pipeline.new(c1, c2) }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [84/80]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics/LineLength: Line is too long. [84/80]

end

production(:command, 'word argument_list?') do |word, args|
Expand Down
27 changes: 27 additions & 0 deletions lib/gitsh/pipeline_environment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'delegate'

module Gitsh
class PipelineEnvironment < SimpleDelegator
def self.build_pair(env)
pipe_reader, pipe_writer = IO.pipe
[
new(env, output_stream: pipe_writer),
new(env, input_stream: pipe_reader),
]
end

def initialize(env, options)
super(env)
@input_stream = options[:input_stream]
@output_stream = options[:output_stream]
end

def input_stream
@input_stream || super
end

def output_stream
@output_stream || super
end
end
end
1 change: 1 addition & 0 deletions lib/gitsh/shell_command_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def initialize(command_with_arguments, env)
def run
pid = Process.spawn(
*command_with_arguments,
in: env.input_stream.to_i,
out: env.output_stream.to_i,
err: env.error_stream.to_i
)
Expand Down
9 changes: 9 additions & 0 deletions man/man1/gitsh.1.in
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ execute
.Ic left ,
and then
.Ic right .
.It Ic left | right
execute
.Ic left
and
.Ic right
simultaneously, redirecting the standard output from
.Ic left
to the standard input of
.Ic right .
.El
.Pp
As in
Expand Down
48 changes: 48 additions & 0 deletions spec/integration/pipeline_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'spec_helper'

describe 'Pipeline' do
it 'passes output of first command to second command' do
GitshRunner.interactive do |gitsh|
gitsh.type('init')
gitsh.type('commit --allow-empty --message "Empty commit"')
gitsh.type('log --oneline | !wc -l')

expect(gitsh).to output_no_errors
expect(gitsh).to output /\b1\b/
end
end

it 'runs processes in parallel' do
GitshRunner.interactive do |gitsh|
gitsh.type_without_waiting('!yes hello | !sed -e "s/ello/i/"')
gitsh.wait_for_output
gitsh.send_sigint

expect(gitsh).to output_no_errors
expect(gitsh).to output /hi\nhi\n/
end
end

it 'considers the pipeline to have failed if either command fails' do
GitshRunner.interactive do |gitsh|
gitsh.type(':echo $unset | !wc && :echo Success')

expect(gitsh).to output_error /unset/
expect(gitsh).not_to output /Success/
end
end

it 'supports multi-stage pipelines' do
GitshRunner.interactive do |gitsh|
gitsh.type('init')
gitsh.type('commit --allow-empty -m First --author "A <a@example.com>"')
gitsh.type('commit --allow-empty -m Second --author "B <b@example.com>"')
gitsh.type('commit --allow-empty -m Third --author "A <a@example.com>"')
gitsh.type('commit --allow-empty -m Fourth --author "C <c@example.com>"')
gitsh.type('log --format="%aN" | !sort -u | !wc -l')

expect(gitsh).to output_no_errors
expect(gitsh).to output /\b3\b/
end
end
end
29 changes: 23 additions & 6 deletions spec/support/gitsh_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def self.interactive(options={}, &block)
end

def initialize(options)
@input_stream = RSpec::Mocks::Double.new('STDIN', tty?: true)
@input_stream = RSpec::Mocks::Double.new('STDIN', tty?: true, to_i: 0)
@output_stream = Tempfile.new('stdout')
@error_stream = Tempfile.new('stderr')
@line_editor = Gitsh::LineEditorHistoryFilter.new(FakeLineEditor.new)
Expand All @@ -27,29 +27,46 @@ def initialize(options)
end

def run_interactive
runner = nil
@runner = nil
with_a_temporary_home_directory do
in_a_temporary_directory do
setup_unix_env
runner = start_runner_thread
@runner = start_runner_thread
wait_for_prompt

yield(self)

line_editor.type(':exit')
runner.join
@runner.join
@runner = nil
end
end
rescue RSpec::Expectations::ExpectationNotMetError
runner.kill
runner.join
@runner.kill
@runner.join
@runner = nil
raise
end

def type(string)
type_without_waiting(string)
wait_for_prompt
end

def type_without_waiting(string)
@error_position_before_command = error_stream.pos
@position_before_command = output_stream.pos
line_editor.type(string)
end

def wait_for_output
while output_stream.pos == @position_before_command
sleep 0.001
end
end

def send_sigint
@runner.raise(Interrupt)
wait_for_prompt
end

Expand Down
61 changes: 61 additions & 0 deletions spec/units/commands/pipeline_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require 'spec_helper'
require 'gitsh/commands/pipeline'
require 'gitsh/commands/git_command'
require 'gitsh/commands/shell_command'

describe Gitsh::Commands::Pipeline do
describe '#execute' do
it 'pipes output of left command to right command' do
left_command = create_command_double { 'string' }
right_command = create_command_double { |input| input.upcase }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block.

env = build_env
pipeline = described_class.new(left_command, right_command)

result = pipeline.execute(env)

expect(result).to be true
expect(env.output_stream.string).to eq "STRING\n"
end

context 'when the left command fails' do
it 'returns false' do
left_command = create_command_double(false) { '' }
right_command = create_command_double { |input| input.upcase }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/SymbolProc: Pass &:upcase as an argument to create_command_double instead of a block.

pipeline = described_class.new(left_command, right_command)

result = pipeline.execute(build_env)

expect(result).to be false
end
end

context 'when the right command fails' do
it 'returns false' do
left_command = create_command_double { 'string' }
right_command = create_command_double(false) { '' }
pipeline = described_class.new(left_command, right_command)

result = pipeline.execute(build_env)

expect(result).to be false
end
end
end

def create_command_double(value=true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/SpaceAroundEqualsInParameterDefault: Surrounding space missing in default value assignment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/SpaceAroundEqualsInParameterDefault: Surrounding space missing in default value assignment.

command = instance_double(Gitsh::Commands::GitCommand)
allow(command).to receive(:execute) do |env|
input = env.input_stream.read
env.output_stream.puts yield(input)
value
end
command
end

def build_env
Gitsh::Environment.new(
input_stream: instance_double(IO, read: ""),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

output_stream: StringIO.new
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call.

)
end
end
5 changes: 5 additions & 0 deletions spec/units/lexer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
to produce_tokens ['WORD(foo)', 'OR', 'WORD(bar)', 'EOS']
end

it 'recognises the | operator' do
expect('foo | bar').
to produce_tokens ['WORD(foo)', 'PIPE', 'WORD(bar)', 'EOS']
end

it 'recognises newlines' do
expect("foo\nbar").
to produce_tokens ['WORD(foo)', 'EOL', 'WORD(bar)', 'EOS']
Expand Down
8 changes: 8 additions & 0 deletions spec/units/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@
expect(result).to be_a(Gitsh::Commands::Tree::Multi)
end

it 'parses two commands combined with |' do
result = parse(tokens(
[:WORD, 'log'], [:PIPE], [:WORD, '!wc'], [:EOS],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/FirstParameterIndentation: Indent the first parameter one step more than tokens(.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout/FirstParameterIndentation: Indent the first parameter one step more than tokens(.

))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/TrailingCommaInArguments: Put a comma after the last parameter of a multiline method call.


expect(result).to be_a(Gitsh::Commands::Pipeline)
end

it 'parses two commands combined with newlines' do
result = parse(tokens(
[:WORD, 'add'], [:SPACE], [:WORD, '.'],
Expand Down
Loading