vershunt 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|