rip 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENSE +20 -0
- data/README.markdown +306 -0
- data/Rakefile +51 -0
- data/bin/rip +6 -0
- data/examples/debug.rb +13 -0
- data/examples/reverse.rb +21 -0
- data/ext/extconf.rb +11 -0
- data/lib/rip.rb +53 -0
- data/lib/rip/commands.rb +113 -0
- data/lib/rip/commands/build.rb +23 -0
- data/lib/rip/commands/core.rb +82 -0
- data/lib/rip/commands/install.rb +37 -0
- data/lib/rip/commands/uninstall.rb +43 -0
- data/lib/rip/env.rb +128 -0
- data/lib/rip/installer.rb +130 -0
- data/lib/rip/memoize.rb +111 -0
- data/lib/rip/package.rb +126 -0
- data/lib/rip/package_api.rb +94 -0
- data/lib/rip/package_manager.rb +175 -0
- data/lib/rip/packages/dir_package.rb +34 -0
- data/lib/rip/packages/file_package.rb +60 -0
- data/lib/rip/packages/gem_package.rb +44 -0
- data/lib/rip/packages/git_package.rb +62 -0
- data/lib/rip/packages/http_package.rb +46 -0
- data/lib/rip/packages/remote_gem_package.rb +64 -0
- data/lib/rip/packages/ripfile_package.rb +46 -0
- data/lib/rip/setup.rb +205 -0
- data/lib/rip/sh/git.rb +35 -0
- data/lib/rip/ui.rb +24 -0
- data/lib/rip/version.rb +9 -0
- data/setup.rb +27 -0
- data/test/commands_test.rb +15 -0
- data/test/dev.rip +2 -0
- data/test/dir_test.rb +25 -0
- data/test/env_test.rb +173 -0
- data/test/git_test.rb +36 -0
- data/test/mock_git.rb +51 -0
- data/test/repos/simple_c/dot_git/HEAD +1 -0
- data/test/repos/simple_c/dot_git/config +6 -0
- data/test/repos/simple_c/dot_git/description +1 -0
- data/test/repos/simple_c/dot_git/hooks/applypatch-msg.sample +15 -0
- data/test/repos/simple_c/dot_git/hooks/commit-msg.sample +24 -0
- data/test/repos/simple_c/dot_git/hooks/post-commit.sample +8 -0
- data/test/repos/simple_c/dot_git/hooks/post-receive.sample +15 -0
- data/test/repos/simple_c/dot_git/hooks/post-update.sample +8 -0
- data/test/repos/simple_c/dot_git/hooks/pre-applypatch.sample +14 -0
- data/test/repos/simple_c/dot_git/hooks/pre-commit.sample +18 -0
- data/test/repos/simple_c/dot_git/hooks/pre-rebase.sample +169 -0
- data/test/repos/simple_c/dot_git/hooks/prepare-commit-msg.sample +36 -0
- data/test/repos/simple_c/dot_git/hooks/update.sample +107 -0
- data/test/repos/simple_c/dot_git/index +0 -0
- data/test/repos/simple_c/dot_git/info/exclude +6 -0
- data/test/repos/simple_c/dot_git/logs/HEAD +1 -0
- data/test/repos/simple_c/dot_git/logs/refs/heads/master +1 -0
- data/test/repos/simple_c/dot_git/objects/2d/94227280db3ac66875f52592c6a736b4526084 +0 -0
- data/test/repos/simple_c/dot_git/objects/3f/1d6dacdedf75058e9edf23f48de03fa451f7ce +1 -0
- data/test/repos/simple_c/dot_git/objects/53/23e9a7ff897fe7bfc3357d9274775b67f9ade4 +0 -0
- data/test/repos/simple_c/dot_git/objects/55/31db58bd71148661c400dc48815bf06b366128 +0 -0
- data/test/repos/simple_c/dot_git/objects/d7/55c6f119520808609a8d79bac1a8dbe0c57424 +0 -0
- data/test/repos/simple_c/dot_git/objects/d7/58739ea968ac8e8fe0cbbf1f6451af47f04964 +0 -0
- data/test/repos/simple_c/dot_git/objects/e6/7b81a24f0ce4bff84c3599b5c9ba7093f554d8 +0 -0
- data/test/repos/simple_c/dot_git/objects/ec/be0a80dc841c16beb2c733bbdd320b45565d89 +0 -0
- data/test/repos/simple_c/dot_git/refs/heads/master +1 -0
- data/test/repos/simple_c/ext/simp/extconf.rb +6 -0
- data/test/repos/simple_c/ext/simp/simp.c +17 -0
- data/test/repos/simple_c/lib/simple_c.rb +1 -0
- data/test/repos/simple_d-1.2.3/lib/simple_d.rb +1 -0
- data/test/rip_test.rb +8 -0
- data/test/test_helper.rb +79 -0
- data/test/ui_test.rb +36 -0
- metadata +149 -0
data/lib/rip/sh/git.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rip
|
2
|
+
module Sh
|
3
|
+
module Git
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def git_ls_remote(source, version)
|
7
|
+
`git ls-remote #{source} #{version} 2> /dev/null`
|
8
|
+
end
|
9
|
+
|
10
|
+
def git_clone(source, cache_name)
|
11
|
+
`git clone #{source} #{cache_name}`
|
12
|
+
end
|
13
|
+
|
14
|
+
def git_fetch(remote)
|
15
|
+
`git fetch #{remote}`
|
16
|
+
end
|
17
|
+
|
18
|
+
def git_reset_hard(version)
|
19
|
+
`git reset --hard #{version}`
|
20
|
+
end
|
21
|
+
|
22
|
+
def git_submodule_init
|
23
|
+
`git submodule init`
|
24
|
+
end
|
25
|
+
|
26
|
+
def git_submodule_update
|
27
|
+
`git submodule update`
|
28
|
+
end
|
29
|
+
|
30
|
+
def git_revparse(repothing)
|
31
|
+
`git rev-parse #{repothing}`
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rip/ui.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rip
|
2
|
+
class UI
|
3
|
+
def initialize(io=nil)
|
4
|
+
@io = io
|
5
|
+
end
|
6
|
+
|
7
|
+
def puts(*args)
|
8
|
+
return unless @io
|
9
|
+
|
10
|
+
if args.empty?
|
11
|
+
@io.puts ""
|
12
|
+
else
|
13
|
+
args.each { |msg| @io.puts(msg) }
|
14
|
+
end
|
15
|
+
|
16
|
+
@io.flush
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def abort(msg)
|
21
|
+
@io && Kernel.abort("rip: #{msg}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/rip/version.rb
ADDED
data/setup.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# installs rip like so:
|
3
|
+
# ruby setup.rb
|
4
|
+
#
|
5
|
+
# also uninstalls rip like so:
|
6
|
+
# ruby setup.rb uninstall
|
7
|
+
#
|
8
|
+
# probably requires sudo.
|
9
|
+
#
|
10
|
+
|
11
|
+
__DIR__ = File.expand_path(File.dirname(__FILE__))
|
12
|
+
$LOAD_PATH.unshift File.join(__DIR__, 'lib')
|
13
|
+
|
14
|
+
require 'rip'
|
15
|
+
|
16
|
+
include Rip::Setup
|
17
|
+
|
18
|
+
if ARGV.include? 'uninstall'
|
19
|
+
uninstall :verbose
|
20
|
+
elsif ARGV.include? 'reinstall'
|
21
|
+
uninstall
|
22
|
+
install
|
23
|
+
elsif installed?
|
24
|
+
puts "rip: already installed"
|
25
|
+
else
|
26
|
+
install
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
context 'Parsing command line args' do
|
5
|
+
setup do
|
6
|
+
Rip::Commands.send(:public, :parse_args)
|
7
|
+
end
|
8
|
+
|
9
|
+
test "works" do
|
10
|
+
assert_equal ["install", { :f => true }, []], Rip::Commands.parse_args(%w( install -f ))
|
11
|
+
assert_equal ["install", { :f => "force" }, []], Rip::Commands.parse_args(%w( install -f=force ))
|
12
|
+
assert_equal ["install", { :f => true }, [ "force", "name" ]], Rip::Commands.parse_args(%w( install -f force name ))
|
13
|
+
assert_equal ["install", {}, [ "something" ]], Rip::Commands.parse_args(%w( install something ))
|
14
|
+
end
|
15
|
+
end
|
data/test/dev.rip
ADDED
data/test/dir_test.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
context 'Installing from a directory' do
|
5
|
+
setup_with_fs do
|
6
|
+
@source = fresh_local_dir('simple_d-1.2.3').source
|
7
|
+
end
|
8
|
+
|
9
|
+
test "installs the lib files" do
|
10
|
+
Rip::Commands.install({}, @source)
|
11
|
+
|
12
|
+
libpath = Rip.dir + '/active/lib/simple_d.rb'
|
13
|
+
assert File.exists?(libpath), 'simple_d.rb should be installed'
|
14
|
+
end
|
15
|
+
|
16
|
+
test "finds version from name suffix" do
|
17
|
+
assert_equal '1.2', fresh_local_dir('simple_d-1.2').version
|
18
|
+
assert_equal '1.2.3', fresh_local_dir('simple_d-1.2.3').version
|
19
|
+
assert_equal '1.2.3.4', fresh_local_dir('simple_d-1.2.3.4').version
|
20
|
+
end
|
21
|
+
|
22
|
+
test "defaults to unversioned if not named properly" do
|
23
|
+
assert_equal 'unversioned', fresh_local_dir('simple_d').version
|
24
|
+
end
|
25
|
+
end
|
data/test/env_test.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
context "Creating a ripenv" do
|
5
|
+
setup_with_fs do
|
6
|
+
@active_dir = File.join(Rip.dir, 'active')
|
7
|
+
@name = 'new_env'
|
8
|
+
@ripenv = File.join(Rip.dir, @name)
|
9
|
+
assert !File.exists?(@ripenv)
|
10
|
+
end
|
11
|
+
|
12
|
+
test "creates the directories on disk" do
|
13
|
+
Rip::Env.create(@name)
|
14
|
+
assert File.exists?(File.join(@ripenv, 'bin'))
|
15
|
+
assert File.exists?(File.join(@ripenv, 'lib'))
|
16
|
+
end
|
17
|
+
|
18
|
+
test "confirms creation" do
|
19
|
+
assert_equal "created new_env", Rip::Env.create(@name)
|
20
|
+
end
|
21
|
+
|
22
|
+
test "fails if the ripenv exists" do
|
23
|
+
assert_equal "base exists", Rip::Env.create('base')
|
24
|
+
end
|
25
|
+
|
26
|
+
test "switches to the new ripenv" do
|
27
|
+
Rip::Env.create(@name)
|
28
|
+
assert_equal @name, Rip::Env.active
|
29
|
+
end
|
30
|
+
|
31
|
+
test 'fails if no ripenv is given' do
|
32
|
+
assert_equal 'must give a ripenv to create', Rip::Env.create('')
|
33
|
+
assert_not_equal '', Rip::Env.active
|
34
|
+
assert_equal 'must give a ripenv to create', Rip::Env.create(' ')
|
35
|
+
assert_equal 'must give a ripenv to create', Rip::Env.create("\t")
|
36
|
+
assert_equal 'must give a ripenv to create', Rip::Env.create("\t ")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "Using a ripenv" do
|
41
|
+
setup_with_fs do
|
42
|
+
@active_dir = File.join(Rip.dir, 'active')
|
43
|
+
@name = 'other'
|
44
|
+
@ripenv = File.join(Rip.dir, @name)
|
45
|
+
@base = 'base'
|
46
|
+
@old_ripenv = File.join(Rip.dir, @base)
|
47
|
+
end
|
48
|
+
|
49
|
+
test "switches the active symlink" do
|
50
|
+
Rip::Env.use(@name)
|
51
|
+
assert_equal @name, Rip::Env.active
|
52
|
+
end
|
53
|
+
|
54
|
+
test "confirms the change" do
|
55
|
+
assert_equal "using #{@name}", Rip::Env.use(@name)
|
56
|
+
end
|
57
|
+
|
58
|
+
test "fails if the new env doesn't exist" do
|
59
|
+
assert_equal "fake doesn't exist", Rip::Env.use("fake")
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'fails if no ripenv is given' do
|
63
|
+
assert_equal "must give a ripenv to use", Rip::Env.use('')
|
64
|
+
assert_not_equal '', Rip::Env.active
|
65
|
+
assert_equal 'must give a ripenv to use', Rip::Env.use(' ')
|
66
|
+
assert_equal 'must give a ripenv to use', Rip::Env.use("\t")
|
67
|
+
assert_equal 'must give a ripenv to use', Rip::Env.use("\t ")
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "Deleting a ripenv" do
|
73
|
+
setup_with_fs do
|
74
|
+
@name = "other"
|
75
|
+
@ripenv = File.join(Rip.dir, @name)
|
76
|
+
end
|
77
|
+
|
78
|
+
test "removes the ripenv" do
|
79
|
+
assert File.exists?(@ripenv)
|
80
|
+
Rip::Env.delete(@name)
|
81
|
+
assert !File.exists?(@ripenv)
|
82
|
+
end
|
83
|
+
|
84
|
+
test "confirms removal" do
|
85
|
+
assert_equal "deleted #{@name}", Rip::Env.delete(@name)
|
86
|
+
end
|
87
|
+
|
88
|
+
test "fails if it's the active ripenv" do
|
89
|
+
assert_equal "can't delete active environment", Rip::Env.delete('base')
|
90
|
+
end
|
91
|
+
|
92
|
+
test "fails if it doesn't exist" do
|
93
|
+
name = 'fake_env'
|
94
|
+
assert_equal "can't find #{name}", Rip::Env.delete(name)
|
95
|
+
assert File.exists?(Rip.dir)
|
96
|
+
end
|
97
|
+
|
98
|
+
test "fails if no name is provided" do
|
99
|
+
assert_equal "must give a ripenv to delete", Rip::Env.delete('')
|
100
|
+
assert_equal 'must give a ripenv to delete', Rip::Env.delete(' ')
|
101
|
+
assert_equal 'must give a ripenv to delete', Rip::Env.delete("\t")
|
102
|
+
assert_equal 'must give a ripenv to delete', Rip::Env.delete("\t ")
|
103
|
+
assert File.exists?(Rip.dir)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "Listing ripenvs" do
|
108
|
+
setup_with_fs do
|
109
|
+
@ripenvs = Rip::Env.list
|
110
|
+
end
|
111
|
+
|
112
|
+
test "prints ripenvs" do
|
113
|
+
assert_equal 2, @ripenvs.split(' ').size
|
114
|
+
assert @ripenvs.include?('base')
|
115
|
+
end
|
116
|
+
|
117
|
+
test "ignores the active symlink" do
|
118
|
+
assert !@ripenvs.include?('active')
|
119
|
+
end
|
120
|
+
|
121
|
+
test "ignores rip-* directories" do
|
122
|
+
assert !@ripenvs.include?('rip-packages')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "Displaying the active ripenv" do
|
127
|
+
setup_with_fs do
|
128
|
+
# no setup
|
129
|
+
end
|
130
|
+
|
131
|
+
test "works" do
|
132
|
+
assert_equal 'base', Rip::Env.active
|
133
|
+
end
|
134
|
+
|
135
|
+
test "works across env changes" do
|
136
|
+
Rip::Env.use('other')
|
137
|
+
assert_equal 'other', Rip::Env.active
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "Copying the current ripenv" do
|
142
|
+
setup_with_fs do
|
143
|
+
@name = 'new_env'
|
144
|
+
@ripenv = File.join(Rip.dir, @name)
|
145
|
+
end
|
146
|
+
|
147
|
+
test "creates a new env" do
|
148
|
+
Rip::Env.copy(@name)
|
149
|
+
assert File.exists?(@ripenv)
|
150
|
+
end
|
151
|
+
|
152
|
+
test "switches to the new env" do
|
153
|
+
Rip::Env.copy(@name)
|
154
|
+
assert_equal @name, Rip::Env.active
|
155
|
+
end
|
156
|
+
|
157
|
+
test "makes the new env a copy of the active env"
|
158
|
+
|
159
|
+
test "fails if the new env exists" do
|
160
|
+
assert_equal "other exists", Rip::Env.copy('other')
|
161
|
+
end
|
162
|
+
|
163
|
+
test "confirms the creation" do
|
164
|
+
assert_equal "cloned base to #{@name}", Rip::Env.copy(@name)
|
165
|
+
end
|
166
|
+
|
167
|
+
test 'fails if no new ripenv is given' do
|
168
|
+
assert_equal 'must give a ripenv to copy to', Rip::Env.copy('')
|
169
|
+
assert_equal 'must give a ripenv to copy to', Rip::Env.copy(' ')
|
170
|
+
assert_equal 'must give a ripenv to copy to', Rip::Env.copy("\t")
|
171
|
+
assert_equal 'must give a ripenv to copy to', Rip::Env.copy("\t ")
|
172
|
+
end
|
173
|
+
end
|
data/test/git_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
context 'Installing from a remote git repo' do
|
5
|
+
setup_with_fs do
|
6
|
+
@source = fresh_remote_git('simple_c')
|
7
|
+
end
|
8
|
+
|
9
|
+
teardown do
|
10
|
+
Rip::GitPackage.unmock_git
|
11
|
+
end
|
12
|
+
|
13
|
+
test "installs the lib files" do
|
14
|
+
Rip::Commands.install({}, @source)
|
15
|
+
|
16
|
+
libpath = Rip.dir + '/active/lib/simple_c.rb'
|
17
|
+
assert File.exists?(libpath), 'simple_c.rb should be installed'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'Installing from a local git repo' do
|
22
|
+
setup_with_fs do
|
23
|
+
@sources = fresh_local_git('simple_c')
|
24
|
+
end
|
25
|
+
|
26
|
+
teardown do
|
27
|
+
Rip::GitPackage.unmock_git
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'local installs the lib files' do
|
31
|
+
Rip::Commands.install({}, @sources)
|
32
|
+
libpath = Rip.dir + '/active/lib/simple_c.rb'
|
33
|
+
assert File.exists?(libpath), 'simple_c.rb should be installed'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/test/mock_git.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Rip
|
2
|
+
module Sh
|
3
|
+
module MockGit
|
4
|
+
def git_ls_remote(source, version)
|
5
|
+
match_errors = []
|
6
|
+
if source != real_source
|
7
|
+
match_errors << "source was #{source} instead of #{real_source}"
|
8
|
+
end
|
9
|
+
|
10
|
+
if !match_errors.empty?
|
11
|
+
raise match_errors.join(" and ")
|
12
|
+
end
|
13
|
+
|
14
|
+
"67be542ddad55c502daf12fde4f784d88a248617\tHEAD\n67be542ddad55c502daf12fde4f784d88a248617\trefs/heads/master"
|
15
|
+
end
|
16
|
+
|
17
|
+
def git_fetch(repothing)
|
18
|
+
end
|
19
|
+
|
20
|
+
def git_clone(source, cache_name)
|
21
|
+
match_errors = []
|
22
|
+
if source != real_source
|
23
|
+
match_errors << "source was #{source} instead of #{real_source}"
|
24
|
+
end
|
25
|
+
if !match_errors.empty?
|
26
|
+
raise match_errors.join(" and ")
|
27
|
+
end
|
28
|
+
|
29
|
+
FakeFS::FileSystem.clone(repo_path(real_repo_name))
|
30
|
+
FileUtils.mv(repo_path(real_repo_name), cache_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def git_submodule_init
|
34
|
+
end
|
35
|
+
|
36
|
+
def git_submodule_update
|
37
|
+
end
|
38
|
+
|
39
|
+
def git_reset_hard(version)
|
40
|
+
end
|
41
|
+
|
42
|
+
def real_repo_name
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
def real_source
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
ref: refs/heads/master
|
@@ -0,0 +1 @@
|
|
1
|
+
Unnamed repository; edit this file to name it for gitweb.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# An example hook script to check the commit log message taken by
|
4
|
+
# applypatch from an e-mail message.
|
5
|
+
#
|
6
|
+
# The hook should exit with non-zero status after issuing an
|
7
|
+
# appropriate message if it wants to stop the commit. The hook is
|
8
|
+
# allowed to edit the commit message file.
|
9
|
+
#
|
10
|
+
# To enable this hook, rename this file to "applypatch-msg".
|
11
|
+
|
12
|
+
. git-sh-setup
|
13
|
+
test -x "$GIT_DIR/hooks/commit-msg" &&
|
14
|
+
exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
|
15
|
+
:
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# An example hook script to check the commit log message.
|
4
|
+
# Called by git-commit with one argument, the name of the file
|
5
|
+
# that has the commit message. The hook should exit with non-zero
|
6
|
+
# status after issuing an appropriate message if it wants to stop the
|
7
|
+
# commit. The hook is allowed to edit the commit message file.
|
8
|
+
#
|
9
|
+
# To enable this hook, rename this file to "commit-msg".
|
10
|
+
|
11
|
+
# Uncomment the below to add a Signed-off-by line to the message.
|
12
|
+
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
13
|
+
# hook is more suited to it.
|
14
|
+
#
|
15
|
+
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
16
|
+
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
17
|
+
|
18
|
+
# This example catches duplicate Signed-off-by lines.
|
19
|
+
|
20
|
+
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
21
|
+
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
22
|
+
echo >&2 Duplicate Signed-off-by lines.
|
23
|
+
exit 1
|
24
|
+
}
|