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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
data.tar.gz: 489aaa7a9417ff0c038fc4bc149ecfc1dd465cc18ea3ecc99384f4e63fe51b70ee7bf918937cd7a569e1715edfc000d4ee88392e67d37bd772b4e255291348b8
|
4
|
+
metadata.gz: 41ad772239c464ba17b89dd070a1b8df7801edcd93511ac3db554f6a5b84ac08b13e57082542f8624041e4c4951da6c33f944ca992a2c270c9c80763108fa2eb
|
5
|
+
SHA1:
|
6
|
+
data.tar.gz: d86f32da8656675a8ad473d55466d4ef551ad276
|
7
|
+
metadata.gz: a60005aa2a2a9bab6c5c31801ce8379ce1a43aac
|
data/bin/vershunt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
INSTALL_DIR='/usr/lib/vershunt'
|
4
|
+
INSTALL_BIN='/usr/bin/vershunt'
|
5
|
+
|
6
|
+
lib_dir =
|
7
|
+
if __FILE__ == INSTALL_BIN
|
8
|
+
# smells like debian, rely on fixed installation locations
|
9
|
+
INSTALL_DIR + '/lib'
|
10
|
+
else
|
11
|
+
# local dev copy, or installed via rubygems
|
12
|
+
require 'rubygems'
|
13
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../lib"))
|
14
|
+
end
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift(lib_dir)
|
17
|
+
|
18
|
+
require 'msp_release'
|
19
|
+
MSPRelease::CLI.run(ARGV)
|
data/lib/msp_release.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module MSPRelease
|
4
|
+
|
5
|
+
require 'climate'
|
6
|
+
require 'msp_release/exec'
|
7
|
+
|
8
|
+
include Exec::Helpers
|
9
|
+
|
10
|
+
module Helpers
|
11
|
+
|
12
|
+
PROJECT_FILE = ".msp_project"
|
13
|
+
|
14
|
+
# Slush bucket for stuff :)
|
15
|
+
|
16
|
+
def msp_version
|
17
|
+
project.version
|
18
|
+
end
|
19
|
+
|
20
|
+
def git_version
|
21
|
+
(m = /release-(.+)/.match(git.cur_branch)) && m[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def time; @time ||= Time.now; end
|
25
|
+
|
26
|
+
def timestamp
|
27
|
+
@timestamp ||= time.strftime("%Y%m%d%H%M%S")
|
28
|
+
end
|
29
|
+
|
30
|
+
def time_rfc
|
31
|
+
offset = time.gmt_offset / (60 * 60)
|
32
|
+
gmt_offset = "#{offset < 0 ? '-' : '+'}#{offset.abs.to_s.rjust(2, "0")}00"
|
33
|
+
time.strftime("%a, %d %b %Y %H:%M:%S #{gmt_offset}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def author
|
37
|
+
@author ||=
|
38
|
+
begin
|
39
|
+
name, email = ['name', 'email'].map {|f| `git config --get user.#{f}`.strip}
|
40
|
+
Author.new(name, email)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def changelog
|
45
|
+
@changelog ||= begin
|
46
|
+
project.respond_to?(:changelog) && project.changelog
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_release_branch?
|
51
|
+
!!/release/.match(git.cur_branch)
|
52
|
+
end
|
53
|
+
|
54
|
+
def data
|
55
|
+
@data ||= {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def data=(data_hash)
|
59
|
+
@data = data_hash
|
60
|
+
end
|
61
|
+
|
62
|
+
def data_exists?
|
63
|
+
File.exists?(DATAFILE)
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_data
|
67
|
+
@data = File.open(DATAFILE, 'r') {|f| YAML.load(f) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def save_data
|
71
|
+
File.open(DATAFILE, 'w') {|f| YAML.dump(@data, f) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def remove_data
|
75
|
+
File.delete(DATAFILE) if File.exists?(DATAFILE)
|
76
|
+
end
|
77
|
+
|
78
|
+
def fail_if_modified_wc
|
79
|
+
annoying_files = git.modified_files + git.added_files
|
80
|
+
if annoying_files.length > 0
|
81
|
+
$stderr.puts("You have modified files in your working copy, and that just won't do")
|
82
|
+
annoying_files.each {|f|$stderr.puts(" " + f)}
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fail_if_push_pending
|
88
|
+
if data_exists?
|
89
|
+
$stderr.puts("You have a release commit pending to be pushed")
|
90
|
+
$stderr.puts("Please push, or reset the operation")
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
include Helpers
|
97
|
+
|
98
|
+
require 'msp_release/log'
|
99
|
+
require 'msp_release/debian'
|
100
|
+
require 'msp_release/git'
|
101
|
+
require 'msp_release/options'
|
102
|
+
require 'msp_release/project'
|
103
|
+
require 'msp_release/build'
|
104
|
+
require 'msp_release/make_branch'
|
105
|
+
require 'msp_release/cli'
|
106
|
+
|
107
|
+
MSP_VERSION_FILE = "lib/msp/version.rb"
|
108
|
+
DATAFILE = ".msp_release"
|
109
|
+
|
110
|
+
LOG = Log.new
|
111
|
+
|
112
|
+
VERSION_SEGMENTS = [:major, :minor, :bugfix]
|
113
|
+
Version = Struct.new(*VERSION_SEGMENTS)
|
114
|
+
Version.module_eval do
|
115
|
+
|
116
|
+
def format(opts={})
|
117
|
+
opts[:without_bugfix] ? "#{major}.#{minor}" : "#{major}.#{minor}.#{bugfix}"
|
118
|
+
end
|
119
|
+
|
120
|
+
alias :to_s :format
|
121
|
+
|
122
|
+
def self.from_string(str)
|
123
|
+
match = /([0-9]+)\.([0-9]+)\.([0-9]+)/.match(str)
|
124
|
+
match && new(*(1..3).map{|i|match[i]})
|
125
|
+
end
|
126
|
+
|
127
|
+
def bump(segment)
|
128
|
+
|
129
|
+
raise ArgumentError, "no such segment: #{segment}" unless VERSION_SEGMENTS.include?(segment)
|
130
|
+
|
131
|
+
reset = false
|
132
|
+
new_segments = VERSION_SEGMENTS.map do |cur_seg|
|
133
|
+
part = self.send(cur_seg)
|
134
|
+
if cur_seg == segment
|
135
|
+
reset = true
|
136
|
+
part.to_i + 1
|
137
|
+
else
|
138
|
+
reset ? 0 : part
|
139
|
+
end.to_s
|
140
|
+
end
|
141
|
+
|
142
|
+
"new_segments: #{new_segments}"
|
143
|
+
|
144
|
+
self.class.new(*new_segments)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
Author = Struct.new(:name, :email)
|
149
|
+
|
150
|
+
extend self
|
151
|
+
|
152
|
+
end
|
153
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class Build
|
3
|
+
|
4
|
+
class NoChangesFileError < StandardError ; end
|
5
|
+
|
6
|
+
include Exec::Helpers
|
7
|
+
|
8
|
+
def initialize(basedir, project, options={})
|
9
|
+
@basedir = basedir
|
10
|
+
@project = project
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
@sign = options.fetch(:sign, true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform_from_cli!
|
17
|
+
LOG.debug("Building package...")
|
18
|
+
|
19
|
+
result =
|
20
|
+
begin
|
21
|
+
self.perform!
|
22
|
+
rescue Exec::UnexpectedExitStatus => e
|
23
|
+
raise CLI::Exit, "build failed:\n#{e.stderr}"
|
24
|
+
rescue Build::NoChangesFileError => e
|
25
|
+
raise CLI::Exit, "Unable to find changes file with version: " +
|
26
|
+
"#{e.message}\nAvailable: \n" +
|
27
|
+
self.available_changes_files.map { |f| " #{f}" }.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
result.tap do
|
31
|
+
LOG.debug("Package built: #{result.package}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform!
|
36
|
+
dir = File.expand_path(@basedir)
|
37
|
+
raise "directory does not exist: #{dir}" unless
|
38
|
+
File.directory?(dir)
|
39
|
+
|
40
|
+
e = Exec.new(:name => 'build', :quiet => false, :status => :any)
|
41
|
+
Dir.chdir(@project.dir) do
|
42
|
+
e.exec(build_command)
|
43
|
+
end
|
44
|
+
|
45
|
+
if e.last_exitstatus != 0
|
46
|
+
LOG.warn("Warning: #{build_command} exited with #{e.last_exitstatus}")
|
47
|
+
end
|
48
|
+
|
49
|
+
@project.build_result(output_directory)
|
50
|
+
end
|
51
|
+
|
52
|
+
def output_directory
|
53
|
+
File.expand_path(@project.config[:deb_output_directory] ||
|
54
|
+
File.join(@basedir, '..'))
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build_command
|
60
|
+
@project.build_command
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
module CLI
|
3
|
+
|
4
|
+
# Base class for command line operations
|
5
|
+
class Command < Climate::Command
|
6
|
+
include Exec::Helpers
|
7
|
+
end
|
8
|
+
|
9
|
+
# alias this class for shorter lines
|
10
|
+
Exit = Climate::ExitException
|
11
|
+
|
12
|
+
# root of the command hierarchy
|
13
|
+
class Root < Climate::Command('vershunt')
|
14
|
+
|
15
|
+
description """
|
16
|
+
Manipulate your git repository by creating commits and performing
|
17
|
+
branch management to create a consistent log of commits to be used
|
18
|
+
as part of a repeatable build system, as well as encouraging
|
19
|
+
semantic versioning (see http://semver.org).
|
20
|
+
|
21
|
+
Projects must include a .msp_project, which is a yaml file that must at least
|
22
|
+
not be empty. If the project file has a ruby_version_file key, then this
|
23
|
+
file will be considered as well as the debian changelog when updating version
|
24
|
+
information.
|
25
|
+
"""
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Commands that require a git working copy can include this module
|
30
|
+
module WorkingCopyCommand
|
31
|
+
|
32
|
+
include Helpers
|
33
|
+
|
34
|
+
attr_accessor :project, :git
|
35
|
+
|
36
|
+
def initialize(options, leftovers)
|
37
|
+
super
|
38
|
+
|
39
|
+
if File.exists?(PROJECT_FILE)
|
40
|
+
@project = MSPRelease::Project.new_from_project_file(PROJECT_FILE)
|
41
|
+
else
|
42
|
+
raise Climate::ExitException.
|
43
|
+
new("No #{PROJECT_FILE} present in current directory")
|
44
|
+
end
|
45
|
+
|
46
|
+
@git = Git.new(@project, @options)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# hardcoded list of commands
|
52
|
+
COMMANDS = ['help', 'new', 'push', 'branch', 'status', 'reset', 'bump', 'checkout', 'build']
|
53
|
+
|
54
|
+
# These are available on the CLI module
|
55
|
+
module ClassMethods
|
56
|
+
|
57
|
+
attr_reader :commands
|
58
|
+
|
59
|
+
def run(args)
|
60
|
+
init_commands
|
61
|
+
|
62
|
+
Climate.with_standard_exception_handling do
|
63
|
+
begin
|
64
|
+
Root.run(args)
|
65
|
+
rescue Exec::UnexpectedExitStatus => e
|
66
|
+
$stderr.puts(e.message)
|
67
|
+
$stderr.puts(e.stderr)
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def init_commands
|
74
|
+
@commands = {}
|
75
|
+
COMMANDS.each do |name|
|
76
|
+
require "msp_release/cli/#{name}"
|
77
|
+
camel_name =
|
78
|
+
name.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join
|
79
|
+
|
80
|
+
command = MSPRelease::CLI.const_get(camel_name)
|
81
|
+
@commands[name] = command
|
82
|
+
command.set_name(name)
|
83
|
+
command.subcommand_of(Root)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
self.extend(ClassMethods)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class CLI::Branch < CLI::Command
|
3
|
+
include CLI::WorkingCopyCommand
|
4
|
+
|
5
|
+
description """
|
6
|
+
Create and switch to a release branch for the version on HEAD
|
7
|
+
|
8
|
+
The release branch will be named after the project version returned by
|
9
|
+
`vershunt status`, excluding the bugfix version. If the version is
|
10
|
+
1.2.3, a branch of release-1.2 will be created.
|
11
|
+
|
12
|
+
The minor version on master is bumped after the branch is created, although this can be disabled.
|
13
|
+
"""
|
14
|
+
|
15
|
+
opt :allow_non_master_branch, "Allow release branch to be created " +
|
16
|
+
"even if you are not on master. Normally you would not want to do" +
|
17
|
+
" this, so this is here to prevent branches from mistakenly being " +
|
18
|
+
"created from branches other than master",
|
19
|
+
{
|
20
|
+
:short => 'a',
|
21
|
+
:default => false
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
opt :no_bump_master, "Do not bump the minor version of master " +
|
26
|
+
"as part of creating the release branch. Typically after creating a " +
|
27
|
+
"release branch, the minor version being stabilised now lives on the " +
|
28
|
+
"branch and master is now a new version",
|
29
|
+
{
|
30
|
+
:short => 'n',
|
31
|
+
:default => false
|
32
|
+
}
|
33
|
+
|
34
|
+
def run
|
35
|
+
fail_if_push_pending
|
36
|
+
|
37
|
+
# take the version before we do any bumping
|
38
|
+
version = project.version
|
39
|
+
|
40
|
+
branch_from =
|
41
|
+
if git.on_master?
|
42
|
+
bump_and_push_master
|
43
|
+
else
|
44
|
+
check_branching_ok!
|
45
|
+
end
|
46
|
+
|
47
|
+
branch_name = project.branch_name(version)
|
48
|
+
|
49
|
+
begin
|
50
|
+
MSPRelease::MakeBranch.new(git, branch_name, :start_point => branch_from).
|
51
|
+
perform!
|
52
|
+
rescue MSPRelease::MakeBranch::BranchExistsError => e
|
53
|
+
raise CLI::Exit, "A branch already exists for #{version}"
|
54
|
+
end
|
55
|
+
|
56
|
+
$stdout.puts("Switched to release branch '#{branch_name}'")
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_branching_ok!
|
60
|
+
if options[:allow_non_master_branch]
|
61
|
+
$stderr.puts("Creating a non-master release branch, --allow-non-master-branch supplied")
|
62
|
+
"HEAD@{0}"
|
63
|
+
else
|
64
|
+
raise CLI::Exit, "You must be on master to create " +
|
65
|
+
"release branches, or pass --allow-non-master-branch"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def bump_and_push_master
|
70
|
+
|
71
|
+
return "HEAD@{0}" if options[:no_bump_master]
|
72
|
+
|
73
|
+
# don't let this happen with a dirty working copy, because we reset the
|
74
|
+
# master branch, which will kill all your changes
|
75
|
+
fail_if_modified_wc
|
76
|
+
|
77
|
+
new_version, *changed_files = project.bump_version('minor')
|
78
|
+
exec "git add -- #{changed_files.join(' ')}"
|
79
|
+
|
80
|
+
# FIXME dry this part up, perhaps using a bump operation class
|
81
|
+
exec "git commit -m 'BUMPED VERSION TO #{new_version}'"
|
82
|
+
|
83
|
+
begin
|
84
|
+
$stdout.puts "Bumping master to #{new_version}, pushing to origin..."
|
85
|
+
exec "git push origin master"
|
86
|
+
rescue
|
87
|
+
$stderr.puts "error pushing bump commit to master, undoing bump..."
|
88
|
+
exec "git reset --hard HEAD@{1}"
|
89
|
+
raise CLI::Exit, 'could not push bump commit to master, if you do ' +
|
90
|
+
'not want to bump the minor version of master, try again with ' +
|
91
|
+
'--no-bump-master'
|
92
|
+
end
|
93
|
+
|
94
|
+
"HEAD@{1}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module MSPRelease
|
2
|
+
class CLI::Build < CLI::Command
|
3
|
+
include Debian::Versions
|
4
|
+
|
5
|
+
# When cloning repositories, limit to this many commits from each head
|
6
|
+
CLONE_DEPTH = 5
|
7
|
+
|
8
|
+
description """Build debian packages suitable for deployment fresh from
|
9
|
+
source control using a build commit or a development version.
|
10
|
+
|
11
|
+
When no BRANCH_NAME is given, or that branch is not a release branch, the
|
12
|
+
latest commit from that branch is checked out and the changelog version is
|
13
|
+
adjusted to show this is a development build.
|
14
|
+
|
15
|
+
If BRANCH_NAME denotes a release branch (i.e release-1.1) then the latest
|
16
|
+
/release/ commit is checked out, even if there are commits after it.
|
17
|
+
The changelog remains unaltered in this case - the source tree should have
|
18
|
+
the correct version information in it.
|
19
|
+
"""
|
20
|
+
|
21
|
+
arg :git_url, "URL used to clone the git repository"
|
22
|
+
|
23
|
+
arg :branch_name, "Name of a branch to build from, defaults to ${default}",
|
24
|
+
:default => "master"
|
25
|
+
|
26
|
+
opt :sign, "Pass options to dpkg-buildpackage to tell it whether or not to sign the build products",
|
27
|
+
{
|
28
|
+
:short => 'S',
|
29
|
+
:default => false
|
30
|
+
}
|
31
|
+
|
32
|
+
opt :shallow, "Only perform a shallow checkout to a depth of five" +
|
33
|
+
"commits from each head. See git documentation for more details",
|
34
|
+
{
|
35
|
+
:short => 's',
|
36
|
+
:default => true
|
37
|
+
}
|
38
|
+
|
39
|
+
opt :distribution, "Specify the debian distribution to put in the " +
|
40
|
+
"changelog when checking out a development version",
|
41
|
+
{
|
42
|
+
:short => 'd',
|
43
|
+
:long => 'debian-distribution',
|
44
|
+
:type => :string
|
45
|
+
}
|
46
|
+
|
47
|
+
opt :verbose, "Print some useful debugging output to stdout",
|
48
|
+
{
|
49
|
+
:long => 'verbose',
|
50
|
+
:short => 'v', :default => false
|
51
|
+
}
|
52
|
+
|
53
|
+
opt :noisy, "Output dpkg-buildpackage output to stderr",
|
54
|
+
{
|
55
|
+
:short => 'n', :default => false
|
56
|
+
}
|
57
|
+
|
58
|
+
opt :silent, "Do not print out build products to stdout", {
|
59
|
+
:default => false
|
60
|
+
}
|
61
|
+
|
62
|
+
def run
|
63
|
+
git_url = arguments[:git_url]
|
64
|
+
release_spec_arg = arguments[:branch_name]
|
65
|
+
|
66
|
+
do_build = options[:build]
|
67
|
+
tar_it = options[:tar]
|
68
|
+
clone_depth = options[:shallow] ? CLONE_DEPTH : nil
|
69
|
+
|
70
|
+
LOG.silent if options[:silent]
|
71
|
+
LOG.verbose if options[:verbose]
|
72
|
+
LOG.noisy if options[:noisy]
|
73
|
+
|
74
|
+
branch_name = release_spec_arg || 'master'
|
75
|
+
|
76
|
+
shallow_output = clone_depth.nil?? '' : ' (shallow)'
|
77
|
+
|
78
|
+
options[:shallow_output] = shallow_output
|
79
|
+
|
80
|
+
# checkout project
|
81
|
+
tmp_dir = "vershunt-#{Time.now.to_i}.tmp"
|
82
|
+
Git.clone(git_url, {:depth => clone_depth, :out_to => tmp_dir,
|
83
|
+
:branch => branch_name, :no_single_branch => true,
|
84
|
+
:exec => {:quiet => true}})
|
85
|
+
|
86
|
+
project = Project.new_from_project_file(tmp_dir + "/" + Helpers::PROJECT_FILE)
|
87
|
+
|
88
|
+
project.prepare_for_build(branch_name, options)
|
89
|
+
|
90
|
+
if project.respond_to?(:build)
|
91
|
+
project.build(options)
|
92
|
+
else
|
93
|
+
raise "I don't know how to build this project"
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|