vershunt 2.0.4
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/bin/vershunt +19 -0
- data/lib/msp_release.rb +153 -0
- data/lib/msp_release/build.rb +64 -0
- data/lib/msp_release/cli.rb +90 -0
- data/lib/msp_release/cli/branch.rb +97 -0
- data/lib/msp_release/cli/build.rb +99 -0
- data/lib/msp_release/cli/bump.rb +49 -0
- data/lib/msp_release/cli/checkout.rb +196 -0
- data/lib/msp_release/cli/distrib.rb +7 -0
- data/lib/msp_release/cli/help.rb +14 -0
- data/lib/msp_release/cli/new.rb +62 -0
- data/lib/msp_release/cli/push.rb +29 -0
- data/lib/msp_release/cli/reset.rb +17 -0
- data/lib/msp_release/cli/status.rb +46 -0
- data/lib/msp_release/debian.rb +229 -0
- data/lib/msp_release/exec.rb +120 -0
- data/lib/msp_release/git.rb +145 -0
- data/lib/msp_release/log.rb +48 -0
- data/lib/msp_release/make_branch.rb +29 -0
- data/lib/msp_release/options.rb +35 -0
- data/lib/msp_release/project.rb +41 -0
- data/lib/msp_release/project/base.rb +63 -0
- data/lib/msp_release/project/debian.rb +168 -0
- data/lib/msp_release/project/gem.rb +74 -0
- data/lib/msp_release/project/git.rb +76 -0
- data/lib/msp_release/project/ruby.rb +40 -0
- data/lib/msp_release/version.rb +3 -0
- metadata +116 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'popen4'
|
3
|
+
|
4
|
+
module MSPRelease
|
5
|
+
class Exec
|
6
|
+
|
7
|
+
class UnexpectedExitStatus < StandardError
|
8
|
+
def initialize(expected, actual, cmd, output, stdout, stderr)
|
9
|
+
@expected = expected
|
10
|
+
@actual = actual
|
11
|
+
@command = cmd
|
12
|
+
@stdout = stdout
|
13
|
+
@stderr = stderr
|
14
|
+
super("command '#{command}' exited with #{actual}, expected #{expected}")
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :expect, :actual, :command, :output, :stdout, :stderr
|
18
|
+
# this marries up with what popen et al return
|
19
|
+
alias :exitstatus :actual
|
20
|
+
end
|
21
|
+
|
22
|
+
module Helpers
|
23
|
+
def exec(command, options={})
|
24
|
+
# use the options we were constructed with if they exist
|
25
|
+
if respond_to?(:options)
|
26
|
+
options[:quiet] = !self.options.verbose? unless options.has_key? :quiet
|
27
|
+
end
|
28
|
+
|
29
|
+
if respond_to? :exec_name
|
30
|
+
options[:name] = exec_name unless options.has_key? :name
|
31
|
+
end
|
32
|
+
|
33
|
+
MSPRelease::Exec.exec(command, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.exec(command, options={})
|
38
|
+
new(options).exec(command, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :name
|
42
|
+
|
43
|
+
def initialize(options={})
|
44
|
+
@name = options.fetch(:name, nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
def last_stdout
|
48
|
+
@last_stdout
|
49
|
+
end
|
50
|
+
|
51
|
+
def last_stderr
|
52
|
+
@last_stderr
|
53
|
+
end
|
54
|
+
|
55
|
+
def last_output
|
56
|
+
@last_output
|
57
|
+
end
|
58
|
+
|
59
|
+
def last_exitstatus
|
60
|
+
@last_exitstatus
|
61
|
+
end
|
62
|
+
|
63
|
+
def exec(command, options={})
|
64
|
+
|
65
|
+
expected = to_expected_array(options.fetch(:status, 0))
|
66
|
+
|
67
|
+
@last_stdout = ""
|
68
|
+
@last_stderr = ""
|
69
|
+
@last_output = ""
|
70
|
+
|
71
|
+
output = options[:output]
|
72
|
+
|
73
|
+
output_semaphore = Mutex.new
|
74
|
+
|
75
|
+
start = name.nil? ? '' : "#{name}: "
|
76
|
+
|
77
|
+
status = POpen4::popen4(command) do |stdout, stderr, stdin, pid|
|
78
|
+
t1 = Thread.new do
|
79
|
+
stdout.each_line do |line|
|
80
|
+
@last_stdout += line
|
81
|
+
output_semaphore.synchronize { @last_output += line }
|
82
|
+
msg = "#{start}#{line}"
|
83
|
+
(output && !options[:quiet]) ? output.puts(msg) : LOG.debug(msg)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
t2 = Thread.new do
|
88
|
+
stderr.each_line do |line|
|
89
|
+
@last_stderr += line
|
90
|
+
output_semaphore.synchronize { @last_output += line }
|
91
|
+
msg = "#{start}#{line}"
|
92
|
+
(output && !options[:quiet]) ? output.puts(msg) : LOG.error(msg)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
t1.join
|
97
|
+
t2.join
|
98
|
+
end
|
99
|
+
|
100
|
+
@last_exitstatus = status.exitstatus
|
101
|
+
|
102
|
+
unless expected.nil? || expected.include?(status.exitstatus)
|
103
|
+
raise UnexpectedExitStatus.new(expected, status.exitstatus, command, @last_output, @last_stdout, @last_stderr)
|
104
|
+
end
|
105
|
+
|
106
|
+
@last_stdout
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def to_expected_array(value)
|
112
|
+
case value
|
113
|
+
when :any then nil
|
114
|
+
when Array then value
|
115
|
+
when Numeric then [value]
|
116
|
+
else raise ArgumentError, "#{value} is not an acceptable exit status"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class MSPRelease::Git
|
2
|
+
|
3
|
+
# methods that don't require a local clone
|
4
|
+
module ClassMethods
|
5
|
+
def version()
|
6
|
+
MSPRelease::Exec.exec("git version", :output => StringIO.new)
|
7
|
+
end
|
8
|
+
|
9
|
+
def version_gte(maj, min=0, bug=0)
|
10
|
+
match = version.match(/(\d+)\.(\d+)\.(\d+)\..*/)
|
11
|
+
match[1].to_i >= maj &&
|
12
|
+
match[2].to_i >= min &&
|
13
|
+
match[3].to_i >= bug
|
14
|
+
end
|
15
|
+
|
16
|
+
def clone(git_url, options={})
|
17
|
+
out_to = options[:out_to]
|
18
|
+
exec_options = options[:exec] || {}
|
19
|
+
depth_arg = options[:depth].nil?? '' : "--depth #{options[:depth]}"
|
20
|
+
branch = options[:branch].nil?? '' : "--branch #{options[:branch]}"
|
21
|
+
|
22
|
+
heads = StringIO.new
|
23
|
+
MSPRelease::Exec.exec("git ls-remote --heads #{git_url}", :output => heads)
|
24
|
+
|
25
|
+
unless heads.string.match(/#{options[:branch]}/)
|
26
|
+
raise MSPRelease::CLI::Exit, "Git pathspec 'origin/#{options[:branch]}' does not exist"
|
27
|
+
end
|
28
|
+
|
29
|
+
single_branch = if version_gte(1, 7, 10)
|
30
|
+
options[:no_single_branch] ? '--no-single-branch' :
|
31
|
+
options[:single_branch] ? '--single-branch' :
|
32
|
+
''
|
33
|
+
end
|
34
|
+
|
35
|
+
MSPRelease::Exec.
|
36
|
+
exec("git clone #{depth_arg} #{single_branch} #{branch} #{git_url} #{out_to.nil?? '' : out_to}", exec_options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
include ClassMethods
|
42
|
+
end
|
43
|
+
|
44
|
+
class Commit
|
45
|
+
include MSPRelease::Exec::Helpers
|
46
|
+
attr_reader :message
|
47
|
+
attr_reader :author
|
48
|
+
attr_reader :hash
|
49
|
+
|
50
|
+
def initialize(project, attributes)
|
51
|
+
@project = project
|
52
|
+
attributes.each do |k, v|
|
53
|
+
instance_variable_set(:"@#{k}", v)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def release_commit?
|
58
|
+
!! (release_name || tag)
|
59
|
+
end
|
60
|
+
|
61
|
+
def tag
|
62
|
+
tag = exec "git tag --contains #{hash}"
|
63
|
+
(m = /^release-([0-9.]*)$/.match(tag)) && m[1]
|
64
|
+
end
|
65
|
+
|
66
|
+
def release_name
|
67
|
+
@project.release_name_from_message(message) || tag
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
include MSPRelease::Exec::Helpers
|
72
|
+
|
73
|
+
def initialize(project, options)
|
74
|
+
@project = project
|
75
|
+
@options = options
|
76
|
+
end
|
77
|
+
|
78
|
+
def exec_name; 'git'; end
|
79
|
+
|
80
|
+
attr_reader :options
|
81
|
+
|
82
|
+
def on_master?
|
83
|
+
cur_branch == 'master'
|
84
|
+
end
|
85
|
+
|
86
|
+
def cur_branch
|
87
|
+
/^\* (.+)$/.match(exec "git branch")[1]
|
88
|
+
end
|
89
|
+
|
90
|
+
def last_release_tag
|
91
|
+
a = exec "git describe --tags --match 'release-*'"
|
92
|
+
end
|
93
|
+
|
94
|
+
def branch_exists?(branch_name)
|
95
|
+
# use backticks, we don't want an error
|
96
|
+
`git show-branch origin/#{branch_name}`
|
97
|
+
$? == 0
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_and_switch(branch_name, options={})
|
101
|
+
start_point = options[:start_point]
|
102
|
+
exec "git branch #{branch_name} #{start_point}"
|
103
|
+
exec "git push origin #{branch_name}"
|
104
|
+
exec "git checkout #{branch_name}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def added_files
|
108
|
+
status_files("new file")
|
109
|
+
end
|
110
|
+
|
111
|
+
def modified_files
|
112
|
+
status_files("modified")
|
113
|
+
end
|
114
|
+
|
115
|
+
def status_files(status)
|
116
|
+
output = exec "git status", :status => :any
|
117
|
+
pattern = /#{status}: +(.+)$/
|
118
|
+
output.split("\n").map {|l| pattern.match(l) }.compact.map{|m|m[1]}
|
119
|
+
end
|
120
|
+
|
121
|
+
def latest_commit(project)
|
122
|
+
commit_output =
|
123
|
+
exec("git --no-pager log -1 --no-color --full-index --pretty=short").split("\n")
|
124
|
+
|
125
|
+
commit_pattern = /^commit ([a-z0-9]+)$/
|
126
|
+
author_pattern = /^Author: (.+)$/
|
127
|
+
message_pattern = /^ (.+)$/
|
128
|
+
|
129
|
+
hash = commit_output.grep(commit_pattern) {|m| commit_pattern.match(m)[1] }.first
|
130
|
+
author = commit_output.grep(author_pattern) {|m| author_pattern.match(m)[1] }.first
|
131
|
+
message = commit_output.grep(message_pattern).
|
132
|
+
map {|row| message_pattern.match(row)[1] }.
|
133
|
+
join(" ")
|
134
|
+
|
135
|
+
Commit.new(project, {:hash => hash, :author => author, :message => message})
|
136
|
+
end
|
137
|
+
|
138
|
+
def remote_is_ahead?
|
139
|
+
raise NotImplementedError
|
140
|
+
branch_name = cur_branch
|
141
|
+
exec "git fetch"
|
142
|
+
exec "git --no-pager log origin/#{branch_name} --no-color --oneline -1".split("")
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# simple class for keeping output in one place
|
2
|
+
module MSPRelease
|
3
|
+
class Log
|
4
|
+
@@log_levels = {
|
5
|
+
:error => $stderr,
|
6
|
+
:warn => $stderr,
|
7
|
+
:info => $stdout,
|
8
|
+
:debug => StringIO.new,
|
9
|
+
:trace => StringIO.new
|
10
|
+
}
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
end
|
14
|
+
|
15
|
+
def verbose
|
16
|
+
@@log_levels[:debug] = $stdout
|
17
|
+
end
|
18
|
+
|
19
|
+
def noisy
|
20
|
+
@@log_levels[:debug] = $stderr
|
21
|
+
end
|
22
|
+
|
23
|
+
def silent
|
24
|
+
@@log_levels.keys.each do |level|
|
25
|
+
@@log_levels[level] = StringIO.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@@log_levels.keys.each do |level|
|
30
|
+
define_method level do |msg|
|
31
|
+
@@log_levels[level].puts(msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method "set_#{level}" do
|
35
|
+
@@log_levels[level] = $stdout
|
36
|
+
end
|
37
|
+
|
38
|
+
define_method "set_#{level}_err" do
|
39
|
+
@@log_levels[level] = $stderr
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method "unset_#{level}" do
|
43
|
+
@@log_levels[level] = self.nil_log
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class MakeBranch
|
3
|
+
|
4
|
+
class BranchExistsError < StandardError ; end
|
5
|
+
|
6
|
+
include Exec::Helpers
|
7
|
+
|
8
|
+
attr_reader :branch_name
|
9
|
+
attr_reader :git
|
10
|
+
attr_reader :options
|
11
|
+
|
12
|
+
def initialize(git, branch_name, options={})
|
13
|
+
@git = git
|
14
|
+
@branch_name = branch_name
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform!
|
19
|
+
if git.branch_exists?(branch_name)
|
20
|
+
raise BranchExistsError, branch_name
|
21
|
+
end
|
22
|
+
|
23
|
+
git.create_and_switch(branch_name,
|
24
|
+
:start_point => options[:start_point])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class MSPRelease::Options
|
4
|
+
|
5
|
+
def self.get(given_args)
|
6
|
+
args = given_args.clone
|
7
|
+
opt = new(args)
|
8
|
+
[opt, args]
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(args)
|
12
|
+
@store = {
|
13
|
+
:verbose => false
|
14
|
+
}
|
15
|
+
opts = OptionParser.new do |opts|
|
16
|
+
opts.on("-v", "--[no-]verbose", "Run with verbose output") do |v|
|
17
|
+
@store[:verbose] = v
|
18
|
+
end
|
19
|
+
end.parse!(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key)
|
23
|
+
@store.fetch(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, value)
|
27
|
+
$stderr.puts("Warning, overiding config key #{key}") if @store[key]
|
28
|
+
@store[key] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def verbose?
|
32
|
+
self[:verbose]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module MSPRelease::Project
|
2
|
+
|
3
|
+
def self.new_from_project_file(filename)
|
4
|
+
config = YAML.load_file(filename)
|
5
|
+
dirname = File.expand_path(File.dirname(filename))
|
6
|
+
|
7
|
+
project = Base.new(filename, dirname)
|
8
|
+
|
9
|
+
# TODO: make it so that this doesn't have to know about all the possible
|
10
|
+
# mixins.
|
11
|
+
|
12
|
+
# If the directory has a debian folder, treat it as a debian project.
|
13
|
+
if File.directory?(File.join(dirname, 'debian'))
|
14
|
+
project.extend(Debian)
|
15
|
+
elsif Dir.glob("#{dirname}/*.gemspec").count > 0
|
16
|
+
project.extend(Gem)
|
17
|
+
# If its a gem project, it must also be a ruby project.
|
18
|
+
project.extend(Ruby)
|
19
|
+
end
|
20
|
+
|
21
|
+
# If there is a ruby version file, we treat it as a
|
22
|
+
if config[:ruby_version_file]
|
23
|
+
project.extend(Ruby)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Git project
|
27
|
+
if File.directory?(File.join(dirname, '.git'))
|
28
|
+
project.extend(Git)
|
29
|
+
end
|
30
|
+
|
31
|
+
project
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'msp_release/project/base'
|
37
|
+
require 'msp_release/project/ruby'
|
38
|
+
require 'msp_release/project/debian'
|
39
|
+
require 'msp_release/project/gem'
|
40
|
+
require 'msp_release/project/git'
|
41
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class Project::Base
|
3
|
+
|
4
|
+
include MSPRelease::Exec::Helpers
|
5
|
+
|
6
|
+
RELEASE_COMMIT_PREFIX = "RELEASE COMMIT - "
|
7
|
+
|
8
|
+
attr_reader :config, :config_file, :dir
|
9
|
+
|
10
|
+
def initialize(filename, dir='.')
|
11
|
+
@config_file = filename
|
12
|
+
@dir = dir
|
13
|
+
@config = YAML.load_file(@config_file)
|
14
|
+
config.each do |key, value|
|
15
|
+
instance_variable_set("@#{key}", value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def release_name_from_message(commit_message)
|
20
|
+
idx = commit_message.index(RELEASE_COMMIT_PREFIX)
|
21
|
+
return nil unless idx == 0
|
22
|
+
|
23
|
+
commit_message[RELEASE_COMMIT_PREFIX.length..-1]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the commit message that should be used for a given release
|
27
|
+
def release_commit_message(release_name)
|
28
|
+
"#{RELEASE_COMMIT_PREFIX}#{release_name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def branch_name(version=self.version)
|
32
|
+
"release-#{version.format(:without_bugfix => true)}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def at_version?(rhs_version)
|
36
|
+
any_version == rhs_version
|
37
|
+
end
|
38
|
+
|
39
|
+
def bump_version(segment)
|
40
|
+
new_version = version.bump(segment.to_sym)
|
41
|
+
written_files = write_version(new_version)
|
42
|
+
[new_version, *written_files]
|
43
|
+
end
|
44
|
+
|
45
|
+
def build(options={})
|
46
|
+
unless self.respond_to?(:build_command)
|
47
|
+
raise "Don't know how to build this project"
|
48
|
+
end
|
49
|
+
|
50
|
+
build_opts(options) if self.respond_to?(:build_opts)
|
51
|
+
|
52
|
+
LOG.debug("Checked out to #{@dir}")
|
53
|
+
|
54
|
+
LOG.debug("Building...")
|
55
|
+
build = Build.new(@dir, self, options)
|
56
|
+
|
57
|
+
result = build.perform_from_cli!
|
58
|
+
LOG.debug("Build products:")
|
59
|
+
result.files.each {|f| LOG.info(f) }
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|